├── 1.png
├── 2.png
├── 3.png
├── demo.mp4
├── icon.ico
├── requirements.txt
├── pscripts
├── __pycache__
│ ├── log.cpython-311.pyc
│ ├── sd_api.cpython-311.pyc
│ ├── utils.cpython-311.pyc
│ ├── chat_glm.cpython-311.pyc
│ ├── chat_gpt.cpython-311.pyc
│ ├── init_env.cpython-311.pyc
│ └── process_status.cpython-311.pyc
├── init_env.py
├── lytest.py
├── test.py
├── log.py
├── chat_gpt.py
├── test3.py
├── refresh_options.py
├── test2.py
├── merge_mp4.py
├── utils.py
├── process_status.py
├── sd_api.py
├── chat_glm.py
├── split_file.py
├── ai_prompt.py
├── batch_tts.py
├── gen_video.py
└── ai_tutu.py
├── conf
├── options-default.json
├── options.json
├── config-default.json
└── config.json
├── src
├── pages
│ ├── About.vue
│ ├── Setting.vue
│ ├── App.vue
│ └── App.js
├── main.js
├── router
│ └── index.js
├── Main.vue
└── Main.vue.bak
├── index.html
├── vite.config.js
├── .gitignore
├── options.json
├── README.md
├── electron
├── preload.js
├── index.js
└── ipc_actions.js
├── LICENSE
├── package.json
├── libs
├── build_py_scripts.js
├── task_queue.js
└── utils.js
├── electron-builder.json
└── package-el.js
/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/1.png
--------------------------------------------------------------------------------
/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/2.png
--------------------------------------------------------------------------------
/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/3.png
--------------------------------------------------------------------------------
/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/demo.mp4
--------------------------------------------------------------------------------
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/icon.ico
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/requirements.txt
--------------------------------------------------------------------------------
/pscripts/__pycache__/log.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/log.cpython-311.pyc
--------------------------------------------------------------------------------
/pscripts/__pycache__/sd_api.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/sd_api.cpython-311.pyc
--------------------------------------------------------------------------------
/pscripts/__pycache__/utils.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/utils.cpython-311.pyc
--------------------------------------------------------------------------------
/pscripts/__pycache__/chat_glm.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/chat_glm.cpython-311.pyc
--------------------------------------------------------------------------------
/pscripts/__pycache__/chat_gpt.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/chat_gpt.cpython-311.pyc
--------------------------------------------------------------------------------
/pscripts/__pycache__/init_env.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/init_env.cpython-311.pyc
--------------------------------------------------------------------------------
/pscripts/__pycache__/process_status.cpython-311.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/laure1102/lpanda/HEAD/pscripts/__pycache__/process_status.cpython-311.pyc
--------------------------------------------------------------------------------
/conf/options-default.json:
--------------------------------------------------------------------------------
1 | {
2 | "rolenames": [
3 | ],
4 | "sd_models": [
5 | ],
6 | "sd_vaes": [
7 | ],
8 | "sd_samplers": [
9 | ]
10 | }
--------------------------------------------------------------------------------
/src/pages/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | About page2
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/pscripts/init_env.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import os
4 |
5 | # 获取脚本所在的目录
6 | script_path = os.path.abspath(__file__)
7 | script_dir = os.path.dirname(script_path)
8 | ffmpeg_path = script_dir +"/libs/ffmpeg/bin"
9 | os.environ["PATH"] += os.pathsep + ffmpeg_path
10 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Main from './Main.vue'
3 | import router from './router'
4 | import ViewUI from 'view-design';
5 |
6 | // import style
7 | import 'view-design/dist/styles/iview.css';
8 |
9 | Vue.use(ViewUI);
10 |
11 | new Vue({
12 | router,
13 | render: h => h(Main)
14 | }).$mount('#root')
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LPANDA App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/pscripts/lytest.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | text = """
4 | 奥德赛可家乐福
5 | 啥的房间里
6 |
7 |
8 | 算法邻水的
9 |
10 |
11 |
12 | 啥的机房懒得解释
13 |
14 |
15 |
16 |
17 |
18 | 阿萨德解放路
19 | 是否
20 | 撒旦法
21 |
22 | 胜多负少
23 | """
24 |
25 | # 使用正则表达式匹配只包含空白字符的行,并将其替换为空字符串
26 | cleaned_text = re.sub(r'^\s*$\n', '', text, flags=re.MULTILINE)
27 |
28 | print(cleaned_text)
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | // 配置说明:https://cn.vitejs.dev/config/
2 |
3 | import { createVuePlugin } from 'vite-plugin-vue2'
4 | import path from 'path'
5 |
6 | export default {
7 | resolve: {
8 | alias: {
9 | '@': path.join(__dirname, './src'),
10 | },
11 | extensions: ['.js', '.vue', '.json', '.css', '.ts', '.jsx']
12 | },
13 | base: './',
14 | plugins: [createVuePlugin()]
15 | }
16 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | // 注册路由插件
5 | Vue.use(VueRouter)
6 |
7 | //
8 | const routes = [
9 | {
10 | path: '/',
11 | name: 'Home',
12 | component: () => import('../pages/App.vue')
13 | },
14 | ]
15 |
16 | const router = new VueRouter({
17 | scrollBehavior: () => ({ y: 0 }),
18 | routes
19 | })
20 |
21 | export default router
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | pscripts/libs/*
15 | lexe/*
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
28 | py
29 | build
30 | out
31 |
32 | thumbs.db
33 | !.gitkeep
34 | release/
35 | package-lock.json
36 |
--------------------------------------------------------------------------------
/pscripts/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | import multiprocessing
6 | from process_status import ProcessStatus
7 | import json
8 | from chat_gpt import askChatgpt
9 | from chat_glm import askChatglm
10 | import subprocess
11 | import init_env
12 | from utils import vtt_to_json
13 | import time
14 | from log import logger
15 |
16 |
17 | from gen_video import create_video
18 |
19 | def main(argv):
20 | create_video("cache/2024_03_06_17_04_34_q7aF0zyJ/test.source001.sce")
21 |
22 | if __name__ == "__main__":
23 | main(sys.argv)
24 |
--------------------------------------------------------------------------------
/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "rolenames": [
3 | "zh-HK-HiuGaaiNeural",
4 | "zh-HK-HiuMaanNeural",
5 | "zh-HK-WanLungNeural",
6 | "zh-CN-XiaoxiaoNeural",
7 | "zh-CN-XiaoyiNeural",
8 | "zh-CN-YunjianNeural",
9 | "zh-CN-YunxiNeural",
10 | "zh-CN-YunxiaNeural",
11 | "zh-CN-YunyangNeural",
12 | "zh-CN-liaoning-XiaobeiNeural",
13 | "zh-TW-HsiaoChenNeural",
14 | "zh-TW-YunJheNeural",
15 | "zh-TW-HsiaoYuNeural",
16 | "zh-CN-shaanxi-XiaoniNeural"
17 | ],
18 | "sd_models": [],
19 | "sd_vaes": [],
20 | "sd_samplers": []
21 | }
--------------------------------------------------------------------------------
/conf/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "rolenames": [
3 | "zh-HK-HiuGaaiNeural",
4 | "zh-HK-HiuMaanNeural",
5 | "zh-HK-WanLungNeural",
6 | "zh-CN-XiaoxiaoNeural",
7 | "zh-CN-XiaoyiNeural",
8 | "zh-CN-YunjianNeural",
9 | "zh-CN-YunxiNeural",
10 | "zh-CN-YunxiaNeural",
11 | "zh-CN-YunyangNeural",
12 | "zh-CN-liaoning-XiaobeiNeural",
13 | "zh-TW-HsiaoChenNeural",
14 | "zh-TW-YunJheNeural",
15 | "zh-TW-HsiaoYuNeural",
16 | "zh-CN-shaanxi-XiaoniNeural"
17 | ],
18 | "sd_models": [],
19 | "sd_vaes": [],
20 | "sd_samplers": []
21 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #AI 小说生成推文漫画视频
2 | ##功能
3 | ### 对接SD,GPT自动推理和生图
4 | ### 自动生成视频和字母
5 | ### 只需要提供小说,支持小说字数理论上无上线。
6 | ### 自动调用tts生成语音
7 | ##开发环境准备:
8 | ### 要求python3.10+ ,node.js 14+
9 |
10 | 进入项目目录
11 | ### python -m venv py
12 | ### .\py\Scripts\pip.exe install -r .\requirements.txt
13 |
14 |
15 | ### 下载ffmpeg,放到pscripts/libs目录下。
16 | 也可以到网盘下载ffmpeg:
17 | 通过网盘分享的文件:ffmpeg
18 | 链接: https://pan.baidu.com/s/1hgtTEKsvWGG9k5gYMAwVTw?pwd=pwvs 提取码: pwvs
19 |
20 | npm install
21 | 最后npm start运行程序
22 | 打包成.exe,运行 npm run package
23 |
24 |
25 | 
26 | 
27 |
28 | 
29 |
30 | ##demo视频
31 | [demo视频](demo.mp4)
32 |
--------------------------------------------------------------------------------
/electron/preload.js:
--------------------------------------------------------------------------------
1 | const { contextBridge, ipcRenderer } = require('electron');
2 |
3 | window.addEventListener('DOMContentLoaded', () => {
4 | const replaceText = (selector, text) => {
5 | const element = document.getElementById(selector)
6 | if (element) element.innerText = text
7 | }
8 |
9 | for (const type of ['chrome', 'node', 'electron']) {
10 | replaceText(`${type}-version`, process.versions[type])
11 | }
12 |
13 | console.log('系统进程:', process.versions);
14 | })
15 |
16 |
17 | contextBridge.exposeInMainWorld('electronAPI', {
18 | send: (channel, data) => {
19 | ipcRenderer.send(channel, data);
20 | },
21 | receive: (channel, func) => {
22 | ipcRenderer.once(channel, (event, ...args) => func(...args));
23 | } ,
24 | receiveAways: (channel, func) => {
25 | ipcRenderer.on(channel, (event, ...args) => func(...args));
26 | }
27 | });
--------------------------------------------------------------------------------
/pscripts/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import json
4 |
5 | # 读取配置文件
6 | with open('conf/config.json', 'r', encoding='utf-8') as file:
7 | config = json.load(file)
8 | work_dir = config['work_dir']
9 |
10 | def setup_logger():
11 | # 创建一个日志记录器
12 | logger = logging.getLogger(__name__)
13 | logger.setLevel(logging.DEBUG)
14 |
15 | # 创建一个文件处理器
16 | os.makedirs(f'{work_dir}/logs', exist_ok=True)
17 | file_handler = logging.FileHandler(f'{work_dir}/logs/app.log')
18 | file_handler.setLevel(logging.DEBUG)
19 | # 设置日志的编码为UTF-8
20 | logging.basicConfig(
21 | encoding='utf-8'
22 | )
23 | # 创建一个格式化器并将其添加到处理器
24 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
25 | file_handler.setFormatter(formatter)
26 |
27 | # 将处理器添加到日志记录器
28 | logger.addHandler(file_handler)
29 |
30 | return logger
31 |
32 | # 在模块加载时即配置日志
33 | logger = setup_logger()
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 ziyoren
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
38 |
39 |
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lpanda",
3 | "version": "1.0.1",
4 | "description": "ai 小说生成推文漫画视频",
5 | "keywords": [],
6 | "main": "electron/index.js",
7 | "scripts": {
8 | "start": "concurrently -k \"npm run dev\" \"npm run electron\"",
9 | "dev": "vite",
10 | "dev:el": "cross-env NODE_ENV=development electron .",
11 | "build": "vite build && npm run buildpy",
12 | "preview": "vite preview",
13 | "electron": "wait-on tcp:3000 && npm run dev:el",
14 | "buildpy": "node ./libs/build_py_scripts.js",
15 | "package": "npm run build && node ./package-el.js",
16 | "release": "npm run package"
17 | },
18 | "devDependencies": {
19 | "concurrently": "^7.0.0",
20 | "cross-env": "^7.0.3",
21 | "electron": "^17.2.0",
22 | "electron-builder": "^22.14.13",
23 | "sass": "^1.49.9",
24 | "vite": "^2.8.0",
25 | "wait-on": "^6.0.1",
26 | "electron-packager": "^17.1.2"
27 | },
28 | "dependencies": {
29 | "view-design": "^4.0.0",
30 | "vite-plugin-vue2": "^1.9.3",
31 | "vue": "^2.6.14",
32 | "vue-router": "3.0.1",
33 | "vue-template-compiler": "^2.6.14",
34 | "vuex": "3.0.1",
35 | "winston": "^3.13.0"
36 | },
37 | "author": "laure",
38 | "license": ""
39 | }
40 |
--------------------------------------------------------------------------------
/src/Main.vue.bak:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
41 |
42 |
48 |
--------------------------------------------------------------------------------
/pscripts/chat_gpt.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import requests
4 | import json
5 | from log import logger
6 |
7 | # 读取配置文件
8 | with open('conf/config.json', 'r', encoding='utf-8') as file:
9 | config = json.load(file)
10 | work_dir = config['work_dir']
11 |
12 |
13 | # ChatGPT API Endpoint
14 | api_endpoint = f"{config['gpt_api_url']}/v1/chat/completions"
15 | # API 密钥(替换为你自己的 API 密钥)
16 | api_key = config['gpt_api_key']
17 |
18 | # 请求头,包括 API 密钥和内容类型
19 | headers = {
20 | "Authorization": f"Bearer {api_key}",
21 | "Content-Type": "application/json"
22 | }
23 |
24 |
25 | def askChatgpt(text):
26 | data = {
27 | "model": "gpt-3.5-turbo",
28 | "messages": [
29 | {"role": "system", "content": config['chat_magic_text']},
30 | {"role": "user", "content": text}
31 | ]
32 | }
33 | response = requests.post(api_endpoint, headers=headers, json=data)
34 | if response.status_code == 200:
35 | result = response.json()
36 | assistant_response = result["choices"][0]["message"]["content"]
37 | return assistant_response
38 | else:
39 | logger.info("API 请求失败,HTTP 状态码:", response.status_code)
40 | logger.info("错误信息:", response.text)
41 | return ""
42 |
43 | if __name__ == "__main__":
44 | msg = askChatgpt("你非常聪明")
45 | logger.info(f"{msg}")
--------------------------------------------------------------------------------
/pscripts/test3.py:
--------------------------------------------------------------------------------
1 |
2 | import json
3 | from utils import vtt_to_json
4 | # 读取配置文件
5 | with open('config.json', 'r', encoding='utf-8') as file:
6 | config = json.load(file)
7 |
8 | batchCnt = config['tts_batch_cnt']
9 | scene_split_chars = config['scene_split_chars']
10 |
11 | file_path_sp = "build/2024_01_18_10_32_12_M1VjouHA/1.source001"
12 | file_path_vtt = f"{file_path_sp}.vtt"
13 |
14 | #1解析字幕
15 | zimuArr = vtt_to_json(file_path_vtt)
16 |
17 | print(f"{zimuArr[0]['text']}")
18 |
19 | print(len(zimuArr[0]['text']))
20 |
21 | sceneJsonObjs = {
22 | "objs":[]
23 | }
24 |
25 | scene_seqnum = 0
26 | scene_text = ""
27 | scene_start_index = 0
28 |
29 | for i in range(0, len(zimuArr)):
30 | zimu = zimuArr[i]
31 | scene_text += f"{zimu['text']} "
32 | if len(scene_text) >= scene_split_chars or i == (len(zimuArr) - 1):
33 | scene_seqnum += 1
34 | sceneJsonObj = {}
35 | subZimus = zimuArr[scene_start_index: i+1]
36 | sceneJsonObj['seqnum'] = scene_seqnum
37 | sceneJsonObj['start'] = subZimus[0]['start']
38 | sceneJsonObj['end'] = subZimus[-1]['end']
39 | sceneJsonObj['text'] = scene_text
40 | sceneJsonObj['prompt'] = ''
41 | sceneJsonObj['tutu'] = ''
42 | sceneJsonObjs['objs'].append(sceneJsonObj)
43 | scene_start_index = i + 1
44 | scene_text = ""
45 |
46 |
--------------------------------------------------------------------------------
/libs/build_py_scripts.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { spawn } = require('child_process');
4 |
5 | function listFiles(dirPath,type) {
6 | const files = fs.readdirSync(dirPath);
7 | const pyFiles = files.filter(file => path.extname(file) === type);
8 | return pyFiles;
9 | }
10 |
11 | function runBuild(scriptName,params){
12 | const pythonExecutable = path.join(__dirname, '../py/Scripts/pyinstaller.exe');
13 | const scriptPath = path.join(__dirname, "../pscripts/"+scriptName);
14 | params.unshift(scriptPath);
15 | return spawn(pythonExecutable, params);
16 | }
17 |
18 | const dirPath = './pscripts';
19 | const pythonFiles = listFiles(dirPath,'.py');
20 |
21 | const distDir = "./lexe";
22 | if(fs.existsSync(distDir)){
23 | console.log("删除以前的发布exe");
24 | const oldPyeFiles = listFiles(distDir,'.exe');
25 |
26 | for(let pye of oldPyeFiles){
27 | let filepath = `${distDir}/${pye}`;
28 | try{
29 | fs.unlinkSync(filepath);
30 | }catch(err){
31 | console.log("无法删除文件:" + filepath);
32 | }
33 |
34 | }
35 | }
36 |
37 | for(let pyf of pythonFiles){
38 | const script = runBuild(pyf,["--onefile",`--distpath=${distDir}`,"--specpath=build"]);
39 | script.on('close', (code) => {
40 | console.log(`子进程退出,退出码 ${code}`);
41 | });
42 | }
--------------------------------------------------------------------------------
/electron-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "productName": "YouAppName",
3 | "appId": "YouAppID",
4 | "asar": true,
5 | "directories": {
6 | "output": "release/${version}"
7 | },
8 | "files": [
9 | "electron/**/*",
10 | "dist/**/*"
11 | ],
12 | "electronDownload": {
13 | "mirror": "https://npmmirror.com/mirrors/electron/"
14 | },
15 | "mac": {
16 | "artifactName": "${productName}_${version}.${ext}",
17 | "icon": "build/icons/icon.icns",
18 | "target": [
19 | "dmg"
20 | ]
21 | },
22 | "dmg": {
23 | "contents": [
24 | {
25 | "x": 410,
26 | "y": 150,
27 | "type": "link",
28 | "path": "/Applications"
29 | },
30 | {
31 | "x": 130,
32 | "y": 150,
33 | "type": "file"
34 | }
35 | ]
36 | },
37 | "win": {
38 | "target": [
39 | {
40 | "target": "nsis",
41 | "arch": [
42 | "x64"
43 | ]
44 | }
45 | ],
46 | "artifactName": "${productName}_${version}.${ext}"
47 | },
48 | "nsis": {
49 | "oneClick": false,
50 | "perMachine": false,
51 | "allowToChangeInstallationDirectory": true,
52 | "deleteAppDataOnUninstall": false
53 | }
54 | }
--------------------------------------------------------------------------------
/pscripts/refresh_options.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | import edge_tts
6 | import asyncio
7 | from process_status import ProcessStatus
8 | from utils import vtt_to_json
9 | import json
10 | from sd_api import getModels,getSamplers,getVaes
11 |
12 | from log import logger
13 |
14 | async def main(argv):
15 | config = {}
16 | try:
17 | all_voices = await edge_tts.list_voices()
18 | rolenames = []
19 | for r in all_voices:
20 | locale = r['Locale']
21 | country = locale.split("-")[0]
22 | if country == "zh":
23 | rolenames.append(r['ShortName'])
24 | config['rolenames'] = rolenames
25 | except Exception as e:
26 | logger.info("get rolenames error:")
27 | logger.info(f"{e}")
28 | config['rolenames'] = []
29 | try:
30 | config['sd_models'] = getModels()
31 | except Exception as e:
32 | logger.info("get sd_models error:")
33 | logger.info(f"{e}")
34 | config['sd_models'] = []
35 | try:
36 | config['sd_vaes'] = getVaes()
37 | except Exception as e:
38 | logger.info("get sd_vaes error:")
39 | logger.info(f"{e}")
40 | config['sd_vaes'] = []
41 | try:
42 | config['sd_samplers'] = getSamplers()
43 | except Exception as e:
44 | logger.info("get sd_samplers error:")
45 | logger.info(f"{e}")
46 | config['sd_samplers'] = []
47 | with open('conf/options.json', 'w', encoding='utf-8') as file:
48 | json.dump(config, file, ensure_ascii=False, indent=4)
49 |
50 | if __name__ == "__main__":
51 | asyncio.run(main(sys.argv))
--------------------------------------------------------------------------------
/pscripts/test2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import subprocess
4 | import re
5 | import os
6 | import json
7 | from utils import vtt_to_json
8 |
9 | def get_json_data(filename):
10 | with open(filename, 'r', encoding='utf-8') as file:
11 | return json.load(file)
12 |
13 | # 将时间字符串转换为秒
14 | def time_str_to_seconds(time_str):
15 | hms_str, milliseconds = time_str.split('.')
16 | hours, minutes, seconds = hms_str.split(':')
17 | return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(milliseconds) / 1000
18 |
19 | # 主逻辑
20 | def create_video(split_sfile_name):
21 | ssfn_arr = split_sfile_name.split("/")
22 | filename = ssfn_arr[2]
23 | temp_dir = ssfn_arr[1]
24 | jsonData = get_json_data(f"{split_sfile_name}.sce")
25 | objs = jsonData["objs"]
26 | objs[0]['start'] = "00:00:00.000"
27 |
28 | with open(f"{split_sfile_name}.timeline", "w") as f:
29 | for obj in objs:
30 | start_sec = time_str_to_seconds(obj['start'])
31 | end_sec = time_str_to_seconds(obj['end'])
32 | duration = end_sec - start_sec
33 |
34 | f.write(f"file '../../{obj['tutu']}'\n")
35 | f.write(f"duration {duration}\n")
36 | # 重复最后一张图片以确保最后一项的持续时间
37 | f.write(f"file '../../{objs[-1]['tutu']}'\n")
38 | # 构建 FFmpeg 命令
39 | ffmpeg_cmd = [
40 | "ffmpeg",
41 | "-f", "concat",
42 | "-safe", "0",
43 | "-i", f"{split_sfile_name}.timeline",
44 | "-vsync", "vfr",
45 | "-pix_fmt", "yuv420p",
46 | f"{split_sfile_name}.mp4"
47 | ]
48 |
49 | # 执行 FFmpeg 命令
50 | subprocess.run(ffmpeg_cmd)
51 |
52 |
53 | if __name__ == '__main__':
54 | # 生成视频
55 | create_video('build/2024_01_18_10_32_12_M1VjouHA/1.source001')
--------------------------------------------------------------------------------
/libs/task_queue.js:
--------------------------------------------------------------------------------
1 | class TaskQueue {
2 | constructor() {
3 | this.queue = [];
4 | this.isProcessing = null;
5 | }
6 |
7 | addQueue(task,data) {
8 | let taskId = new Date().getTime();
9 | this.queue.push({
10 | taskId,
11 | task,
12 | data,
13 | });
14 | console.log(`加入任务到队列${taskId}`);
15 | console.log(`queue length:${this.queue.length}`);
16 | console.log(this.queue);
17 | console.log("this.isProcessing");
18 | console.log(this.isProcessing);
19 | if (this.isProcessing == null) {
20 | this.tryToRun(); // 尝试运行队列中的任务
21 | }
22 | return taskId;
23 | }
24 |
25 | length() {
26 | if(this.isProcessing == null) {
27 | return this.queue.length;
28 | }else{
29 | return this.queue.length + 1;
30 | }
31 | }
32 |
33 | async tryToRun() {
34 | console.log("tryToRun...");
35 | if (this.queue.length === 0) {
36 | return; // 队列为空,不执行任何任务
37 | }
38 |
39 | // 等待当前处理的任务完成
40 | if(this.isProcessing == null){
41 | // 获取队列中的第一个任务并移除它
42 | const taskQ = this.queue.shift();
43 | this.isProcessing = taskQ;
44 | let task = taskQ.task;
45 | console.log(`开始执行任务${this.isProcessing.taskId}`);
46 | console.log(this.isProcessing);
47 | await task(taskQ.data);
48 | }
49 | }
50 |
51 | async doneProcessing(){
52 | if(!!this.isProcessing){
53 | this.isProcessing = null;
54 | console.log("doneProcessing...");
55 | console.log("set doneProcessing = null");
56 |
57 | this.tryToRun();
58 | }
59 | }
60 | }
61 |
62 | function initQueue(){
63 | return new TaskQueue();
64 | }
65 |
66 | export default initQueue;
--------------------------------------------------------------------------------
/pscripts/merge_mp4.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import os
4 | from process_status import ProcessStatus
5 | import init_env
6 | import sys
7 | import time
8 | import subprocess
9 | import json
10 | from utils import get_json_data
11 |
12 | from log import logger
13 | # 读取配置文件
14 | with open('conf/config.json', 'r', encoding='utf-8') as file:
15 | config = json.load(file)
16 | work_dir = config['work_dir']
17 |
18 | pStatus = ProcessStatus()
19 |
20 | def main(argv):
21 | if len(argv) > 1:
22 | temp_dir = argv[1]
23 | pStatus.set_temp_dir(temp_dir)
24 | jsonData = pStatus.get_json_data()
25 | filename = jsonData["source_file_name"]
26 | logger.info(f"begin the merge_mp4 func:")
27 | logger.info(f"temp_dir:{temp_dir},filename:{filename}")
28 |
29 | sceobj = get_json_data(jsonData["sce_file_path"])
30 | # 要合并的mp4文件列表
31 | mp4_files=[]
32 | for sceObj in sceobj['objs']:
33 | if sceObj['mp4aufile']:
34 | mp4_files.append(sceObj['mp4aufile'])
35 |
36 | pStatus.write_stage("merge_video_start",f"开始合并mp4中间文件,总共{len(mp4_files)}个")
37 | #要按照文件名序号排序
38 | concat_file = f"{work_dir}/cache/{temp_dir}/merge.txt"
39 | with open(concat_file, 'w', encoding='utf-8') as f:
40 | for mp4f in mp4_files:
41 | mp4f_filename = mp4f.replace("\\","/")
42 | # mp4f_filename = mp4f.replace(f"{work_dir}/cache/{temp_dir}/","")
43 | f.write("file '{}'\n".format(mp4f_filename))
44 |
45 | output = f"{work_dir}/cache/{temp_dir}/{filename}.mp4"
46 | # 使用 ffmpeg 合并视频
47 | command = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_file, '-c', 'copy', output]
48 | subprocess.run(command)
49 | pStatus.write_stage("merge_video_done",f"合并完成")
50 |
51 | else:
52 | logger.info("请提供一个文件路径作为参数。")
53 |
54 | if __name__ == "__main__":
55 | main(sys.argv)
56 |
--------------------------------------------------------------------------------
/package-el.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { spawn } = require('child_process');
4 |
5 | function deleteDirectory(dirPath) {
6 | if (fs.existsSync(dirPath)) {
7 | fs.readdirSync(dirPath).forEach(function(file, index) {
8 | const curPath = path.join(dirPath, file);
9 | if (fs.lstatSync(curPath).isDirectory()) { // 如果是文件夹,递归删除
10 | deleteDirectory(curPath);
11 | } else { // 如果是文件,直接删除
12 | fs.unlinkSync(curPath);
13 | }
14 | });
15 | fs.rmdirSync(dirPath); // 删除目录本身
16 | } else {
17 | console.log(`目录 ${dirPath} 不存在`);
18 | }
19 | }
20 |
21 | function copyFile(sourceFilePath,targetDir,targetFilename){
22 | // 目标文件路径(包含文件名)
23 | const targetFilePath = path.join(targetDir, targetFilename);
24 |
25 | fs.copyFile(sourceFilePath, targetFilePath, (err) => {
26 | if (err) {
27 | console.error('复制文件时出错:', err);
28 | } else {
29 | console.log('文件复制成功:', targetFilePath);
30 | }
31 | });
32 | }
33 |
34 | //删除out目录
35 | const distDir = "./out";
36 | deleteDirectory(distDir);
37 |
38 | let appName="lpanda";
39 | let version = "1.0.0"
40 | let platform = "win32";
41 | let arch = "x64";
42 | let appDir = `${distDir}/${appName}-${platform}-${arch}`;
43 |
44 | // 获取当前进程的环境变量
45 | const env = Object.assign({}, process.env);
46 |
47 | let params = ["electron-packager",".",appName, `--platform=${platform}`, `--arch=${arch}`,
48 | `--out=${distDir}`, `--app-version=${version}`,"--icon=icon.ico",
49 | "--overwrite", "--ignore=cache", "--ignore=logs",
50 | "--ignore=build", "--ignore=pscripts", "--ignore=py", "--ignore=release", "--ignore=src", "--ignore=config.json",
51 | "--ignore=options.json", "--electron-version 28.1.3"];
52 |
53 | const script = spawn("npx",params,
54 | {
55 | shell:true,
56 | env: env, // 使用修改后的环境变量
57 | stdio: 'inherit' // 继承父进程的 stdio
58 | }
59 | );
60 |
61 | script.on('close', (code) => {
62 | console.log(`子进程退出,退出码 ${code}`);
63 | console.log("复制文件到" + appDir);
64 | fs.mkdirSync(`${appDir}/conf`);
65 | copyFile("./conf/config-default.json",appDir+"/conf","config.json");
66 | copyFile("./conf/options-default.json",appDir+"/conf","options.json");
67 | });
68 |
--------------------------------------------------------------------------------
/libs/utils.js:
--------------------------------------------------------------------------------
1 | import { Message } from "view-design";
2 | const Utils = {
3 | clone: function (o) {
4 | return $.extend(true, {}, o);
5 | },
6 | copyValues: function (from, to) {
7 | for (let key in from) {
8 | to[key] = from[key];
9 | }
10 | },
11 | dateFormat: function (fmt, date) {
12 | let ret;
13 | const opt = {
14 | "y+": date.getFullYear().toString(), // 年
15 | "m+": (date.getMonth() + 1).toString(), // 月
16 | "d+": date.getDate().toString(), // 日
17 | "H+": date.getHours().toString(), // 时
18 | "M+": date.getMinutes().toString(), // 分
19 | "S+": date.getSeconds().toString(), // 秒
20 | // 有其他格式化字符需求可以继续添加,必须转化成字符串
21 | };
22 | for (let k in opt) {
23 | ret = new RegExp("(" + k + ")").exec(fmt);
24 | if (ret) {
25 | fmt = fmt.replace(
26 | ret[1],
27 | ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0")
28 | );
29 | }
30 | }
31 | return fmt;
32 | },
33 | //所有form的验证
34 | validateForm: function (vm, formId) {
35 | let validate = true;
36 | vm.$refs[formId].validate((valid) => {
37 | if (!valid) {
38 | validate = false;
39 | }
40 | });
41 | return validate;
42 | },
43 | ipcSend(channel,data){
44 | window.electronAPI.send(channel, data);
45 | },
46 | ipcReceive: (channel, func) => {
47 | window.electronAPI.receive(channel, func);
48 | },
49 | ipcReceiveAways: (channel, func) => {
50 | window.electronAPI.receiveAways(channel, func);
51 | },
52 |
53 | generateRandomString(length) {
54 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
55 | let result = '';
56 | for (let i = 0; i < length; i++) {
57 | result += chars.charAt(Math.floor(Math.random() * chars.length));
58 | }
59 | return result;
60 | },
61 | generateTimestampedRandomString() {
62 | const now = new Date();
63 | const year = now.getFullYear();
64 | const month = String(now.getMonth() + 1).padStart(2, '0');
65 | const day = String(now.getDate()).padStart(2, '0');
66 | const hour = String(now.getHours()).padStart(2, '0');
67 | const minute = String(now.getMinutes()).padStart(2, '0');
68 | const second = String(now.getSeconds()).padStart(2, '0');
69 | const timestamp = `${year}_${month}_${day}_${hour}_${minute}_${second}`;
70 | const randomString = this.generateRandomString(8);
71 | return `${timestamp}_${randomString}`;
72 | },
73 | };
74 |
75 | export default Utils;
--------------------------------------------------------------------------------
/electron/index.js:
--------------------------------------------------------------------------------
1 | const { app,Menu,dialog, BrowserWindow, ipcMain } = require('electron')
2 | const path = require('path')
3 | const { setupIpcActions } = require('./ipc_actions');
4 |
5 |
6 | function createWindow () {
7 | const win = new BrowserWindow({
8 | width: 1200,
9 | height: 960,
10 | webPreferences: {
11 | preload: path.join(__dirname, 'preload.js'),
12 | webSecurity: false, // 禁用同源策略(不推荐,除非完全了解风险)
13 | allowLocalFilesAccess: true, // 允许访问本地文件
14 | nodeIntegration: true, // 允许在渲染进程中使用 Node.js
15 | //contextIsolation: false, // 如果 nodeIntegration 为 true,则必须设置此选项为 false
16 |
17 | }
18 | })
19 | // 创建自定义菜单栏
20 | const template = [
21 | {
22 | label: '文件',
23 | submenu: [
24 | {
25 | label: '重新加载',
26 | click: function() {
27 | if (BrowserWindow.getFocusedWindow()) {
28 | BrowserWindow.getFocusedWindow().reload();
29 | }
30 | }
31 | } ,
32 | {
33 | label: '退出',
34 | click: function() {
35 | app.quit();
36 | }
37 | }
38 | ]
39 | },
40 | {
41 | label: '设置',
42 | submenu: [
43 | {
44 | label: '设置',
45 | click: function() {
46 | // 发送事件到渲染进程
47 | win.webContents.send('open-setting-page');
48 | }
49 | },
50 | ]
51 | },
52 | // ... 其他菜单项
53 | {
54 | label: '帮助',
55 | submenu: [
56 | {
57 | label: '控制台',
58 | click: function() {
59 | win.webContents.toggleDevTools();
60 | }
61 | },
62 | {
63 | label: '关于',
64 | click: function() {
65 | dialog.showMessageBox({
66 | type: 'info',
67 | message: '这是帮助信息',
68 | detail: '你可以在这里添加详细的帮助内容。'
69 | });
70 | }
71 | }
72 | ]
73 | },
74 | ];
75 |
76 | const menu = Menu.buildFromTemplate(template);
77 | Menu.setApplicationMenu(menu);
78 |
79 |
80 | let url = process.env.NODE_ENV === 'development' ?
81 | 'http://localhost:3000' :
82 | 'file://' + path.join(__dirname, '../dist/index.html');
83 |
84 | win.loadURL( url )
85 | }
86 |
87 | app.whenReady().then( async () => {
88 | createWindow();
89 | setupIpcActions();
90 |
91 | app.on('activate', () => {
92 | if (BrowserWindow.getAllWindows().length === 0) {
93 | createWindow()
94 | }
95 | })
96 |
97 | })
98 |
99 | app.on('window-all-closed', () => {
100 | if (process.platform !== 'darwin') {
101 | app.quit()
102 | }
103 | })
104 |
105 |
106 |
--------------------------------------------------------------------------------
/pscripts/utils.py:
--------------------------------------------------------------------------------
1 |
2 | import json
3 | from datetime import datetime,timedelta
4 |
5 | def get_json_data(filename):
6 | with open(filename, 'r', encoding='utf-8') as file:
7 | return json.load(file)
8 |
9 |
10 | # 将时间字符串转换为秒
11 | def time_str_to_seconds(time_str):
12 | hms_str, milliseconds = time_str.split('.')
13 | hours, minutes, seconds = hms_str.split(':')
14 | return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(milliseconds) / 1000
15 |
16 | def duration(start_str,end_str):
17 | start_sec = time_str_to_seconds(start_str)
18 | end_sec = time_str_to_seconds(end_str)
19 | duration = end_sec - start_sec
20 | # 对Decimal类型进行四舍五入,保留3位小数
21 | rounded_num = round(duration, 3)
22 | return rounded_num
23 |
24 |
25 |
26 | def add_time_with_milliseconds(time1, time2):
27 | # Function to convert time string to timedelta
28 | def time_str_to_timedelta(time_str):
29 | hours, minutes, seconds = time_str.split(':')
30 | seconds, microseconds = seconds.split('.')
31 | return timedelta(hours=int(hours), minutes=int(minutes), seconds=int(seconds), microseconds=int(microseconds) * 1000)
32 |
33 | # Convert time strings to timedelta objects
34 | timedelta1 = time_str_to_timedelta(time1)
35 | timedelta2 = time_str_to_timedelta(time2)
36 |
37 | # Add the time deltas
38 | total_timedelta = timedelta1 + timedelta2
39 |
40 | # Extract hours, minutes, seconds and microseconds
41 | total_seconds = int(total_timedelta.total_seconds())
42 | hours, remainder = divmod(total_seconds, 3600)
43 | minutes, seconds = divmod(remainder, 60)
44 | microseconds = total_timedelta.microseconds // 1000 # Convert microseconds to milliseconds
45 |
46 | # Format the result as a string
47 | return f"{hours:02}:{minutes:02}:{seconds:02}.{microseconds:03}"
48 |
49 |
50 | def vtt_to_json(vtt_path,start_seqnum = 0,start_dttm = "00:00:00.000"):
51 | try:
52 | subtitles = []
53 | i=0
54 | with open(vtt_path, 'r', encoding='utf-8') as srt_file:
55 | lines = srt_file.read().split('\n\n\n')
56 | for block in lines:
57 | if not block.strip():
58 | continue
59 | block = block.strip().split('\n')
60 | if len(block) >= 2:
61 | time_range = block[0].split(" --> ")
62 | if len(time_range)>1:
63 | start_time, end_time = time_range
64 | # 提取字幕文本
65 | subtitle_text = ''.join(block[1:])
66 | # 创建字幕字典
67 | i+=1
68 | subtitle_dict = {
69 | "seqnum":start_seqnum + i,
70 | "start": add_time_with_milliseconds(start_dttm,start_time),
71 | "end": add_time_with_milliseconds(start_dttm,end_time),
72 | "text": subtitle_text
73 | }
74 | subtitles.append(subtitle_dict)
75 | return subtitles
76 | except Exception as e:
77 | return f"发生错误: {str(e)}"
78 |
79 |
80 |
81 | if __name__ == "__main__":
82 | duration = duration('00:00:00.095','00:00:43.511')
83 | print(f"{duration}")
84 | seconds_per_tutu = 6
85 | print(f"{int(duration/seconds_per_tutu)},{duration%seconds_per_tutu}")
86 | aa = "爱的色放垃圾啊大家是否"
87 | print(f"{not aa}")
88 |
89 |
--------------------------------------------------------------------------------
/pscripts/process_status.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import json
4 | import os
5 | from datetime import datetime
6 | # 读取配置文件
7 | with open('conf/config.json', 'r', encoding='utf-8') as file:
8 | config = json.load(file)
9 | work_dir = config['work_dir']
10 |
11 | defaultJson = {
12 | "temp_dir":"",
13 | "role_name":"zh-CN-YunxiNeural",
14 | "rate":"+10%",
15 | "volume":"+0%",
16 | "source_file_name":"",
17 | "source_file_path":"",
18 | "sce_file_path":"",
19 | "create_dttm":"",
20 | "end_dttm":"",
21 | "scene_split_chars":0,#单个场景的字符数
22 | "current_stage":"",#当前进程的状态
23 | "current_stage_descr":"",#当前进程的状态
24 | "sce_count":0,
25 | "mp3_done_count":0,
26 | "gpt_done_count":0,
27 | "tutu_done_count":0,
28 | "mp4_done_count":0,
29 | "seconds_per_tutu":0 #每张图持续的时间,0 为持续一段字幕结束,单位 秒
30 | }
31 |
32 |
33 |
34 | class ProcessStatus:
35 | def __init__(self, temp_dir=""):
36 | self.temp_dir = temp_dir
37 | self.statusFilename = f"{work_dir}/cache/{self.temp_dir}/status.json"
38 |
39 | def set_temp_dir(self, temp_dir):
40 | self.temp_dir = temp_dir
41 | self.statusFilename = f"{work_dir}/cache/{self.temp_dir}/status.json"
42 |
43 | def open_file2write(self):
44 | if os.path.exists(self.statusFilename):
45 | # 读取现有数据
46 | with open(self.statusFilename, 'r', encoding='utf-8') as file:
47 | return json.load(file)
48 | else:
49 | # 创建新文件并写入数据
50 | with open(self.statusFilename, 'w', encoding='utf-8') as file:
51 | json.dump(defaultJson, file, ensure_ascii=False, indent=4)
52 | return defaultJson
53 |
54 | def get_json_data(self):
55 | with open(self.statusFilename, 'r', encoding='utf-8') as file:
56 | return json.load(file)
57 |
58 | def write_split_files(self,temp_dir,role_name,rate,volume,source_file_path,source_file_name,scene_split_chars,sce_file_path,sce_count, stage="split_done",stage_descr="场景拆分完成"):
59 | jsonData = self.open_file2write()
60 | jsonData["temp_dir"] = temp_dir
61 | jsonData["role_name"] = role_name
62 | jsonData["rate"] = rate
63 | jsonData["volume"] = volume
64 | jsonData["source_file_path"] = source_file_path
65 | jsonData["source_file_name"] = source_file_name
66 | jsonData["scene_split_chars"] = scene_split_chars
67 | jsonData["sce_file_path"] = sce_file_path
68 | jsonData["sce_count"] = sce_count
69 | jsonData["current_stage"] = stage
70 | jsonData["current_stage_descr"] = stage_descr
71 | now = datetime.now()
72 | # 格式化时间
73 | formatted_time = now.strftime("%y-%m-%d %H:%M:%S")
74 | jsonData["create_dttm"] = formatted_time
75 |
76 | with open(self.statusFilename, 'w', encoding='utf-8') as file:
77 | json.dump(jsonData, file, ensure_ascii=False, indent=4)
78 |
79 | def write_stage(self, stage="",stage_descr=""):
80 | jsonData = self.open_file2write()
81 | jsonData["current_stage"] = stage
82 | jsonData["current_stage_descr"] = stage_descr
83 | with open(self.statusFilename, 'w', encoding='utf-8') as file:
84 | json.dump(jsonData, file, ensure_ascii=False, indent=4)
85 |
86 | def update(self,jsonData):
87 | with open(self.statusFilename, 'w', encoding='utf-8') as file:
88 | json.dump(jsonData, file, ensure_ascii=False, indent=4)
89 |
--------------------------------------------------------------------------------
/pscripts/sd_api.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import base64
4 | import datetime
5 | import json
6 | import os
7 | from log import logger
8 |
9 | import requests
10 |
11 | #sd api https://base_url/docs
12 |
13 | # 读取配置文件
14 | with open('conf/config.json', 'r', encoding='utf-8') as file:
15 | config = json.load(file)
16 | work_dir = config['work_dir']
17 |
18 |
19 | def submit_post(url: str, data: dict):
20 | return requests.post(url, data=json.dumps(data))
21 |
22 | def submit_get(url: str):
23 | return requests.get(url)
24 |
25 |
26 | def save_encoded_image(b64_image: str, output_path: str):
27 | # 判断当前目录下是否存在 output 文件夹,如果不存在则创建
28 | # 将文件放入当前目录下的 output 文件夹中
29 | with open(output_path, "wb") as f:
30 | f.write(base64.b64decode(b64_image))
31 | return output_path
32 |
33 |
34 |
35 | def doOnePrompt(dir,filename, prompt,moreParams={}):
36 | if not os.path.exists(dir):
37 | os.mkdir(dir)
38 | #检查文件是否存在,存在直接返回
39 | filepath = f"{filename}.png"
40 | if os.path.exists(filepath):
41 | return filepath
42 |
43 | base_url = config['sd_url']
44 | if base_url.endswith('/'):
45 | base_url = base_url[:-1]
46 | txt2img_url = base_url + "/sdapi/v1/txt2img" # 服务器地址
47 | data = {
48 | 'prompt': prompt,
49 | 'negative_prompt': config["negative_prompt"],
50 | "height":config["height"],
51 | "width":config["width"],
52 | "override_settings": {
53 | "sd_model_checkpoint": config["sd_model_checkpoint"],
54 | "sd_vae": config["sd_vae"]
55 | },
56 | "sampler_index": config["sampler_index"]
57 | }
58 | data.update(moreParams)
59 | # 将 data.prompt 中的文本,删除文件名非法字符,已下划线分隔,作为文件名
60 | response = submit_post(txt2img_url, data)
61 | # logger.info(f"response:{response.json()}")
62 | imgpath = save_encoded_image(response.json()['images'][0], filepath)
63 | logger.info(f"doOnePrompt saved image:{imgpath}")
64 | return imgpath
65 |
66 |
67 | def doPromptBatch(dir,filename, prompt,batchSize=1,moreParams={}):
68 | if not os.path.exists(dir):
69 | os.mkdir(dir)
70 |
71 | base_url = config['sd_url']
72 | if base_url.endswith('/'):
73 | base_url = base_url[:-1]
74 | txt2img_url = base_url + "/sdapi/v1/txt2img" # 服务器地址
75 | data = {'prompt': prompt,
76 | 'negative_prompt': config["negative_prompt"],
77 | "height":config["height"],
78 | "width":config["width"],
79 | "override_settings": {
80 | "sd_model_checkpoint": config["sd_model_checkpoint"],
81 | "sd_vae": config["sd_vae"]
82 | },
83 | "sampler_index": config["sampler_index"],
84 | "batch_size":batchSize
85 | }
86 | data.update(moreParams)
87 | # 将 data.prompt 中的文本,删除文件名非法字符,已下划线分隔,作为文件名
88 | response = submit_post(txt2img_url, data)
89 | #logger.info(f"{response.json()['images']}")
90 | imgpaths = []
91 | for i, img in enumerate(response.json()['images'], 1):
92 | #检查文件是否存在,存在直接返回
93 | filepath = f"{filename}_{i}.png"
94 | if os.path.exists(filepath):
95 | imgpaths.append(filepath)
96 | continue
97 | ipt = save_encoded_image(img, filepath)
98 | logger.info(f"doPromptBatch saved image:{ipt}")
99 | imgpaths.append(ipt)
100 | return imgpaths
101 |
102 |
103 | def getModels():
104 | base_url = config['sd_url']
105 | if base_url.endswith('/'):
106 | base_url = base_url[:-1]
107 | api = base_url + "/sdapi/v1/sd-models" # 服务器地址
108 | response = submit_get(api)
109 | return response.json()
110 |
111 |
112 | def getVaes():
113 | base_url = config['sd_url']
114 | if base_url.endswith('/'):
115 | base_url = base_url[:-1]
116 | api = base_url + "/sdapi/v1/sd-vae" # 服务器地址
117 | response = submit_get(api)
118 | return response.json()
119 |
120 |
121 | def getSamplers():
122 | base_url = config['sd_url']
123 | if base_url.endswith('/'):
124 | base_url = base_url[:-1]
125 | api = base_url + "/sdapi/v1/samplers" # 服务器地址
126 | response = submit_get(api)
127 | return response.json()
128 |
--------------------------------------------------------------------------------
/pscripts/chat_glm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import requests
4 | import json
5 | import re
6 | from log import logger
7 |
8 | # 读取配置文件
9 | with open('conf/config.json', 'r', encoding='utf-8') as file:
10 | config = json.load(file)
11 | work_dir = config['work_dir']
12 |
13 |
14 | def contains_chinese(s):
15 | if s == None:
16 | return False
17 | pattern = re.compile(r'[\u4e00-\u9fff]+')
18 | return bool(pattern.search(s))
19 | def create_chat_completion(model, messages, use_stream=False):
20 | base_url = config['glm_api_url']
21 | if base_url.endswith('/'):
22 | base_url = base_url[:-1]
23 | data = {
24 | "model": model, # 模型名称
25 | "messages": messages, # 会话历史
26 | "stream": use_stream, # 是否流式响应
27 | "max_tokens": 500, # 最多生成字数
28 | "temperature": 0.8, # 温度
29 | "top_p": 0.8, # 采样概率
30 | }
31 |
32 | response = requests.post(f"{base_url}/v1/chat/completions", json=data, stream=use_stream)
33 | if response.status_code == 200:
34 | if use_stream:
35 | # 处理流式响应
36 | for line in response.iter_lines():
37 | if line:
38 | decoded_line = line.decode('utf-8')[6:]
39 | try:
40 | response_json = json.loads(decoded_line)
41 | content = response_json.get("choices", [{}])[0].get("delta", {}).get("content", "")
42 | logger.info(content)
43 | except:
44 | logger.info("Special Token:", decoded_line)
45 | else:
46 | # 处理非流式响应
47 | decoded_line = response.json()
48 | content = decoded_line.get("choices", [{}])[0].get("message", "").get("content", "")
49 | return content
50 | else:
51 | logger.info("Error:", response.status_code)
52 | return None
53 |
54 | def askChatglm(text):
55 | chat_messages = [
56 | {
57 | "role": "system",
58 | "content": config['chat_magic_text'],
59 | }
60 | ]
61 | chat_messages.append({"role": "user", "content": text})
62 | response = create_chat_completion("chatglm3-6b", chat_messages, use_stream=False)
63 | response = response.strip(u"\u200b")
64 | max_retries = 3 # 设置最大重试次数
65 | retries = 0
66 |
67 | while (response== None or response == "" or "敏感" in response ) and retries < max_retries:
68 | retries += 1
69 | logger.info(f"retry times:{retries},last time response:{response},request text:{text}")
70 | try:
71 | response = create_chat_completion("chatglm3-6b", chat_messages, use_stream=False)
72 | response = response.strip(u"\u200b")
73 | except Exception as e:
74 | print(f"An error occurred: {e}")
75 | break # 如果发生异常,退出循环
76 |
77 | #判断如果有中文,再次请求 请翻译成英文。
78 | if response!="" and contains_chinese(response):
79 | translate_messages = [
80 | {
81 | "role": "system",
82 | "content": "假设你现在是一名优秀的翻译师,擅长将中文翻译成英文",
83 | }
84 | ]
85 | logger.info(f"this response contains chiense char, to translate, the response before translate:{response}")
86 | translate_messages.append({"role": "user", "content": f"请将<<>>中的文字翻译成英文:<<{response}>>"});
87 | response = create_chat_completion("chatglm3-6b", translate_messages, use_stream=False)
88 | logger.info(f"the response after translate:{response}")
89 |
90 | if response== None or response== "":
91 | translate_messages = [
92 | {
93 | "role": "system",
94 | "content": "假设你现在是一名优秀的翻译师,擅长将中文翻译成英文",
95 | }
96 | ]
97 | logger.info(f"this response is null, to translate text, the response before translate:{response}")
98 | translate_messages.append({"role": "user", "content": f"请将<<>>中的文字翻译成英文:<<{text}>>"});
99 | response = create_chat_completion("chatglm3-6b", translate_messages, use_stream=False)
100 | logger.info(f"the response after translate:{response}")
101 |
102 | return response
103 |
104 | if __name__ == "__main__":
105 | while True:
106 | user_input = input("请输入您的问题: ")
107 | response = askChatglm(user_input)
108 | logger.info("回复:", response)
109 | # 可以选择是否在每次循环后清除聊天历史
110 | # chat_messages.pop()
111 |
112 |
--------------------------------------------------------------------------------
/conf/config-default.json:
--------------------------------------------------------------------------------
1 | {
2 | "work_dir": "",
3 | "tts_batch_cnt": 10,
4 | "scene_split_chars": 200,
5 | "prompt_batch_cnt": 1,
6 | "chat_type": "glm",
7 | "gpt_api_url": "",
8 | "gpt_api_key": "",
9 | "glm_api_url": "",
10 | "chat_magic_text": "推理魔法:\n\n# Stable Diffusion prompt 助理\n\n你来充当一位有艺术气息的Stable Diffusion prompt 助理。\n\n## 任务\n\n我用自然语言告诉你要生成的prompt的主题,你的任务是根据这个主题想象一幅完整的画面,然后转化成一份详细的、高质量的prompt,让Stable Diffusion可以生成高质量的图像。\n\n## 背景介绍\n\nStable Diffusion是一款利用深度学习的文生图模型,支持通过使用 prompt 来产生新的图像,描述要包含或省略的元素。\n\n## prompt 概念\n\n- prompt 用来描述图像,由普通常见的单词构成,使用英文半角\",\"做为分隔符。\n- 以\",\"分隔的每个单词或词组称为 tag。所以prompt是由系列由\",\"分隔的tag组成的。\n\n## () 和 [] 语法\n\n调整关键字强度的等效方法是使用 () 和 []。 (keyword) 将tag的强度增加 1.1 倍,与 (keyword:1.1) 相同,最多可加三层。 [keyword] 将强度降低 0.9 倍,与 (keyword:0.9) 相同。\n\n## Prompt 格式要求\n\n下面我将说明 prompt 的生成步骤,这里的 prompt 可用于描述人物、风景、物体或抽象数字艺术图画。你可以根据需要添加合理的、但不少于5处的画面细节。\n\n### 1. prompt 要求\n\n- prompt 内容包含画面主体、材质、附加细节、图像质量、艺术风格、色彩色调、灯光等部分,但你输出的 prompt 不能分段,例如类似\"medium:\"这样的分段描述是不需要的,也不能包含\":\"和\".\"。\n- 画面主体:简短的英文描述画面主体, 如 A girl in a garden,主体细节概括(主体可以是人、事、物、景)画面核心内容。这部分根据我每次给你的主题来生成。你可以添加更多主题相关的合理的细节。\n- 对于人物主题,你必须描述人物的眼睛、鼻子、嘴唇,例如'beautiful detailed eyes,beautiful detailed lips,extremely detailed eyes and face,longeyelashes',以免Stable Diffusion随机生成变形的面部五官,这点非常重要。你还可以描述人物的外表、情绪、衣服、姿势、视角、动作、背景等。人物属性中,1girl表示一个女孩,2girls表示两个女孩。\n- 材质:用来制作艺术品的材料。 例如:插图、油画、3D 渲染和摄影。 Medium 有很强的效果,因为一个关键字就可以极大地改变风格。\n- 附加细节:画面场景细节,或人物细节,描述画面细节内容,让图像看起来更充实和合理。这部分是可选的,要注意画面的整体和谐,不能与主题冲突。\n- 图像质量:这部分内容开头永远要加上“(best quality,4k,8k,highres,masterpiece:1.2),ultra-detailed,(realistic,photorealistic,photo-realistic:1.37)”, 这是高质量的标志。其它常用的提高质量的tag还有,你可以根据主题的需求添加:HDR,UHD,studio lighting,ultra-fine painting,sharp focus,physically-based rendering,extreme detail description,professional,vivid colors,bokeh。\n- 艺术风格:这部分描述图像的风格。加入恰当的艺术风格,能提升生成的图像效果。常用的艺术风格例如:portraits,landscape,horror,anime,sci-fi,photography,concept artists等。\n- 色彩色调:颜色,通过添加颜色来控制画面的整体颜色。\n- 灯光:整体画面的光线效果。\n\n### 2. 限制:\n- tag 内容用英语单词或短语来描述,并不局限于我给你的单词。注意只能包含关键词或词组。\n- 注意不要输出句子,不要有任何解释。\n- tag数量限制40个以内,单词数量限制在60个以内。\n- tag不要带引号(\"\")。\n- 使用英文半角\",\"做分隔符。\n- tag 按重要性从高到低的顺序排列。\n- 我给你的主题可能是用中文描述,你给出的prompt只用英文。\n- 当我给的主题是对话内容,或者你不能分析出场景和画面,就想象两个人谈话的画面。\n- 当我给的主题设计到敏感话题,,就想象出一副风景画面。\n我给你的主题可能是用中文描述,你给出的prompt只用英文。 接下来我每次发送给你的内容,都是一个主题,你需要结合以往所有的主体,请严格遵守以上规则返回我prompt。请务必输出英文,不能包含任何中文文字。\n",
11 | "sd_url": "",
12 | "height": 500,
13 | "width": 768,
14 | "sd_model_checkpoint": "",
15 | "sd_vae": "",
16 | "add_prompt_bef": "",
17 | "add_prompt": "",
18 | "negative_prompt": "nsfw, bottle,bad face, bad anatomy, bad proportions, bad perspective, multiple views, concept art, reference sheet, mutated hands and fingers, interlocked fingers, twisted fingers, excessively bent fingers, more than five fingers, lowres, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, artist name, low quality lowres multiple breasts, low quality lowres mutated hands and fingers, more than two arms, more than two hands, more than two legs, more than two feet, low quality lowres long body, low quality lowres mutation poorly drawn, low quality lowres black-white, low quality lowres bad anatomy, low quality lowres liquid body, low quality lowres liquid tongue, low quality lowres disfigured, low quality lowres malformed, low quality lowres mutated, low quality lowres anatomical nonsense, low quality lowres text font ui, low quality lowres error, low quality lowres malformed hands, low quality lowres long neck, low quality lowres blurred, low quality lowres lowers, low quality lowres low res, low quality lowres bad proportions, low quality lowres bad shadow, low quality lowres uncoordinated body, low quality lowres unnatural body, low quality lowres fused breasts, low quality lowres bad breasts, low quality lowres huge breasts, low quality lowres poorly drawn breasts, low quality lowres extra breasts, low quality lowres liquid breasts, low quality lowres heavy breasts, low quality lowres missing breasts, low quality lowres huge haunch, low quality lowres huge thighs, low quality lowres huge calf, low quality lowres bad hands, low quality lowres fused hand, low quality lowres missing hand, low quality lowres disappearing arms, low quality lowres disappearing thigh, low quality lowres disappearing calf, low quality lowres disappearing legs, low quality lowres fused ears, low quality lowres bad ears, low quality lowres poorly drawn ears, low quality lowres extra ears, low quality lowres liquid ears",
19 | "tutu_batch_cnt": 1,
20 | "seconds_per_tutu": 6,
21 | "sampler_index": "",
22 | "watermark_image": "",
23 | "watermark_position": "topright",
24 | "fps": 10,
25 | "nouse": ""
26 | }
--------------------------------------------------------------------------------
/pscripts/split_file.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | import chardet
6 | from process_status import ProcessStatus
7 | import json
8 | from log import logger
9 | import re
10 |
11 | # 读取配置文件
12 | with open('conf/config.json', 'r', encoding='utf-8') as file:
13 | config = json.load(file)
14 | work_dir = config['work_dir']
15 |
16 | def read_file(file_path):
17 | try:
18 | logger.info(f"begin to read the file:{file_path}")
19 | with open(file_path, 'r', encoding='utf-8') as file:
20 | content = file.read()
21 | logger.info(f"read end")
22 | return content, None
23 | except UnicodeDecodeError:
24 | return None, f"Unicode 解码错误:{e}"
25 | except FileNotFoundError:
26 | return None, "文件未找到。"
27 | except IOError as e:
28 | return None, f"读取文件时发生错误:{e}"
29 |
30 |
31 | def read_file2(file_path):
32 | try:
33 | with open(file_path, 'rb') as file:
34 | logger.info(f"begin to read the file:{file_path}")
35 | raw_data = file.read()
36 | logger.info(f"read end")
37 |
38 | logger.info(f"check the content code start")
39 | encoding = chardet.detect(raw_data)['encoding']
40 | content = raw_data.decode(encoding)
41 | logger.info(f"check the content code end")
42 | return content, None
43 | except FileNotFoundError:
44 | return None, "文件未找到。"
45 | except UnicodeDecodeError as e:
46 | return None, f"Unicode 解码错误:{e}"
47 | except IOError as e:
48 | return None, f"读取文件时发生错误:{e}"
49 |
50 | def split_content(content, max_length=1000):
51 | paragraphs = content.split('\n')
52 | split_contents = []
53 | current_chunk = ""
54 |
55 | for paragraph in paragraphs:
56 | if len(current_chunk) + len(paragraph) < max_length:
57 | current_chunk += paragraph + "\n"
58 | else:
59 | split_contents.append(current_chunk)
60 | current_chunk = paragraph + "\n"
61 | #logger.info("拆分了一次")
62 |
63 | if current_chunk:
64 | split_contents.append(current_chunk)
65 |
66 | return split_contents
67 |
68 | def write_chunks(chunks, base_file_name,temp_dir,role_name,rate,volume, scene_split_chars):
69 | base_name, _ = os.path.splitext(os.path.basename(base_file_name))
70 |
71 | os.makedirs(f'{work_dir}/cache', exist_ok=True)
72 | os.makedirs(f'{work_dir}/cache/{temp_dir}', exist_ok=True)
73 | pStatus = ProcessStatus(temp_dir)
74 |
75 | file_path_scene = f"{work_dir}/cache/{temp_dir}/{base_name}.sce"
76 | sceneJsonObjs = {
77 | "objs":[]
78 | }
79 | for i, chunk in enumerate(chunks, 1):
80 | sceneJsonObj = {}
81 | sceneJsonObj['seqnum'] = i
82 | sceneJsonObj['start'] = ''
83 | sceneJsonObj['end'] = ''
84 | sceneJsonObj['text'] = chunk
85 | sceneJsonObj['prompt'] = ''
86 | sceneJsonObj['tutu'] = []
87 | sceneJsonObj['vttfile'] = ''
88 | sceneJsonObj['srtfile'] = ''
89 | sceneJsonObj['mp3file'] = ''
90 | sceneJsonObj['mp4file'] = ''
91 | sceneJsonObj['mp4aufile'] = ''
92 | sceneJsonObjs['objs'].append(sceneJsonObj)
93 | try:
94 | with open(file_path_scene, 'w', encoding='utf-8') as file:
95 | json.dump(sceneJsonObjs, file, ensure_ascii=False, indent=4)
96 | except IOError as e:
97 | return False, f"写入文件时发生错误:{e}"
98 |
99 | try:
100 | pStatus.write_split_files(temp_dir,role_name,rate,volume,base_file_name,base_name,scene_split_chars,file_path_scene,len(sceneJsonObjs['objs']))
101 | except IOError as e:
102 | return False, f"写入status文件时发生错误:{e}"
103 |
104 | return True, None
105 |
106 |
107 | if __name__ == "__main__":
108 | if len(sys.argv) > 5:
109 | source_path = sys.argv[1]
110 | temp_dir = sys.argv[2]
111 | role_name = sys.argv[3]
112 | rate = sys.argv[4]
113 | volume = sys.argv[5]
114 | logger.info(f"begin the main func:")
115 | content, read_error = read_file(source_path)
116 | # 使用正则表达式匹配只包含空白字符的行,并将其替换为空字符串
117 | content = re.sub(r'^\s*$\n', '', content, flags=re.MULTILINE)
118 | logger.info("完成了读取原文件")
119 | scene_split_chars = config['scene_split_chars']
120 | if content is not None:
121 | chunks = split_content(content,scene_split_chars)
122 | success, write_error = write_chunks(chunks, source_path, temp_dir,role_name,rate,volume, scene_split_chars)
123 | if success:
124 | logger.info("文件内容已成功拆分和写入")
125 | else:
126 | logger.info(write_error)
127 | else:
128 | logger.info(read_error)
129 | else:
130 | logger.info("请提供一个文件路径作为参数。")
131 |
--------------------------------------------------------------------------------
/pscripts/ai_prompt.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | import asyncio
6 | from concurrent.futures import TimeoutError
7 | from process_status import ProcessStatus
8 | import json
9 | from chat_gpt import askChatgpt
10 | from chat_glm import askChatglm
11 | from log import logger
12 | import time
13 | # 读取配置文件
14 | with open('conf/config.json', 'r', encoding='utf-8') as file:
15 | config = json.load(file)
16 | work_dir = config['work_dir']
17 | batchCnt = config['prompt_batch_cnt']
18 |
19 | pStatus = ProcessStatus()
20 |
21 |
22 | def get_json_data(filename):
23 | with open(filename, 'r', encoding='utf-8') as file:
24 | return json.load(file)
25 |
26 | async def runAiPrompt(sceneJsonObjs,sce,temp_dir,source_file_name):
27 | if config['chat_type'] == "gpt":
28 | sce["prompt"] = askChatgpt(sce['text'])
29 | if config['chat_type'] == "glm":
30 | sce["prompt"] = askChatglm(sce['text'])
31 | #time.sleep(10)
32 |
33 | with open(f"{work_dir}/cache/{temp_dir}/{source_file_name}.sce", 'w', encoding='utf-8') as file:
34 | json.dump(sceneJsonObjs, file, ensure_ascii=False, indent=4)
35 |
36 | objs = sceneJsonObjs['objs']
37 | gpt_done_count = 0
38 | for i in range(0, len(objs)):
39 | s = objs[i]
40 | if s['prompt']:
41 | gpt_done_count = gpt_done_count + 1
42 |
43 | jsonData = pStatus.get_json_data()
44 | jsonData['gpt_done_count'] = gpt_done_count
45 | pStatus.update(jsonData)
46 |
47 | async def regenPrompt(temp_dir,seqnum):
48 | pStatus.set_temp_dir(temp_dir)
49 | statusData = pStatus.get_json_data()
50 | sceneJsonObjs = get_json_data(statusData['sce_file_path'])
51 | objs = sceneJsonObjs["objs"]
52 | sce = None
53 | for obj in objs:
54 | if obj["seqnum"] == seqnum:
55 | sce = obj
56 |
57 | if sce != None:
58 | logger.info("do regenPrompt")
59 | logger.info(f"temp_dir:{temp_dir},seqnum:{seqnum},{sce}")
60 | await runAiPrompt(sceneJsonObjs,sce,temp_dir,statusData['source_file_name'])
61 |
62 | async def main(argv):
63 | if len(argv) > 3:
64 | action = argv[1]
65 | if action == "all":
66 | temp_dir = argv[2]
67 | pStatus.set_temp_dir(temp_dir)
68 | jsonData = pStatus.get_json_data()
69 | # 读取场景文件
70 | with open(f"{jsonData['sce_file_path']}", 'r', encoding='utf-8') as file:
71 | sceneJsonObjs = json.load(file)
72 | toPromptSces = []
73 | for sce in sceneJsonObjs['objs']:
74 | #去除掉已经完成的
75 | if(not sce['prompt']):
76 | toPromptSces.append(sce)
77 |
78 | pStatus.write_stage("aiprompt_start",f"将分批进行推理ai提示词,参数: 同时处理个数:{batchCnt}")
79 | timeout_seconds = 60
80 | has_time_error = False
81 |
82 | for i in range(0, len(toPromptSces), batchCnt):
83 | # 获取当前批次的文件列表
84 | current_batch = toPromptSces[i:i + batchCnt]
85 | # 创建并发执行的任务
86 | #tasks = [runAiPrompt(sceneJsonObjs,sce,temp_dir,jsonData["source_file_name"]) for sce in current_batch]
87 | # 并发执行当前批次的任务
88 | #await asyncio.gather(*tasks)
89 | tasks = []
90 | for sce in current_batch:
91 | # 使用 asyncio.wait_for 调用 runTts 并设置超时时间
92 | task = asyncio.wait_for(runAiPrompt(sceneJsonObjs,sce,temp_dir,jsonData["source_file_name"]), timeout=timeout_seconds)
93 | tasks.append(task)
94 |
95 | try:
96 | await asyncio.gather(*tasks)
97 | for sf in current_batch:
98 | logger.info(f"finished {sf['seqnum']}")
99 | except TimeoutError as e:
100 | logger.error(f"Timeout occurred for one or more tasks: {e}")
101 | # 这里可以处理超时的情况,例如重新尝试或跳过超时的任务
102 | has_time_error = True
103 | continue
104 | except Exception as e:
105 | logger.error(f"other error occured: {e}")
106 | # 这里可以处理超时的情况,例如重新尝试或跳过超时的任务
107 | has_time_error = True
108 | continue
109 |
110 | if has_time_error:
111 | await main(argv)
112 | pStatus.write_stage("aiprompt_done",f"推理ai提示词完成")
113 | else:
114 | temp_dir = argv[2]
115 | seqnum = int(argv[3])
116 | #logger.info(f"regen prompt:{temp_dir},{seqnum}")
117 | await regenPrompt(temp_dir,seqnum)
118 | else:
119 | logger.info("请提供一个文件路径作为参数。")
120 |
121 |
122 | if __name__ == "__main__":
123 | asyncio.run(main(sys.argv))
124 |
--------------------------------------------------------------------------------
/conf/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "work_dir": "E:\\ai\\workSpace",
3 | "tts_batch_cnt": 10,
4 | "scene_split_chars": 200,
5 | "prompt_batch_cnt": 1,
6 | "chat_type": "glm",
7 | "gpt_api_url": "http://xx.xx.xx.xx:19999",
8 | "gpt_api_key": "",
9 | "glm_api_url": "https://xxxxxx:8443/",
10 | "chat_magic_text": "推理魔法:\n\n# Stable Diffusion prompt 助理\n\n你来充当一位有艺术气息的Stable Diffusion prompt 助理。\n\n## 任务\n\n我用自然语言告诉你要生成的prompt的主题,你的任务是根据这个主题想象一幅完整的画面,然后转化成一份详细的、高质量的prompt,让Stable Diffusion可以生成高质量的图像。\n\n## 背景介绍\n\nStable Diffusion是一款利用深度学习的文生图模型,支持通过使用 prompt 来产生新的图像,描述要包含或省略的元素。\n\n## prompt 概念\n\n- prompt 用来描述图像,由普通常见的单词构成,使用英文半角\",\"做为分隔符。\n- 以\",\"分隔的每个单词或词组称为 tag。所以prompt是由系列由\",\"分隔的tag组成的。\n\n## () 和 [] 语法\n\n调整关键字强度的等效方法是使用 () 和 []。 (keyword) 将tag的强度增加 1.1 倍,与 (keyword:1.1) 相同,最多可加三层。 [keyword] 将强度降低 0.9 倍,与 (keyword:0.9) 相同。\n\n## Prompt 格式要求\n\n下面我将说明 prompt 的生成步骤,这里的 prompt 可用于描述人物、风景、物体或抽象数字艺术图画。你可以根据需要添加合理的、但不少于5处的画面细节。\n\n### 1. prompt 要求\n\n- prompt 内容包含画面主体、材质、附加细节、图像质量、艺术风格、色彩色调、灯光等部分,但你输出的 prompt 不能分段,例如类似\"medium:\"这样的分段描述是不需要的,也不能包含\":\"和\".\"。\n- 画面主体:简短的英文描述画面主体, 如 A girl in a garden,主体细节概括(主体可以是人、事、物、景)画面核心内容。这部分根据我每次给你的主题来生成。你可以添加更多主题相关的合理的细节。\n- 对于人物主题,你必须描述人物的眼睛、鼻子、嘴唇,例如'beautiful detailed eyes,beautiful detailed lips,extremely detailed eyes and face,longeyelashes',以免Stable Diffusion随机生成变形的面部五官,这点非常重要。你还可以描述人物的外表、情绪、衣服、姿势、视角、动作、背景等。人物属性中,1girl表示一个女孩,2girls表示两个女孩。\n- 材质:用来制作艺术品的材料。 例如:插图、油画、3D 渲染和摄影。 Medium 有很强的效果,因为一个关键字就可以极大地改变风格。\n- 附加细节:画面场景细节,或人物细节,描述画面细节内容,让图像看起来更充实和合理。这部分是可选的,要注意画面的整体和谐,不能与主题冲突。\n- 图像质量:这部分内容开头永远要加上“(best quality,4k,8k,highres,masterpiece:1.2),ultra-detailed,(realistic,photorealistic,photo-realistic:1.37)”, 这是高质量的标志。其它常用的提高质量的tag还有,你可以根据主题的需求添加:HDR,UHD,studio lighting,ultra-fine painting,sharp focus,physically-based rendering,extreme detail description,professional,vivid colors,bokeh。\n- 艺术风格:这部分描述图像的风格。加入恰当的艺术风格,能提升生成的图像效果。常用的艺术风格例如:portraits,landscape,horror,anime,sci-fi,photography,concept artists等。\n- 色彩色调:颜色,通过添加颜色来控制画面的整体颜色。\n- 灯光:整体画面的光线效果。\n\n### 2. 限制:\n- tag 内容用英语单词或短语来描述,并不局限于我给你的单词。注意只能包含关键词或词组。\n- 注意不要输出句子,不要有任何解释。\n- tag数量限制40个以内,单词数量限制在60个以内。\n- tag不要带引号(\"\")。\n- 使用英文半角\",\"做分隔符。\n- tag 按重要性从高到低的顺序排列。\n- 我给你的主题可能是用中文描述,你给出的prompt只用英文。\n- 当我给的主题是对话内容,或者你不能分析出场景和画面,就想象两个人谈话的画面。\n- 当我给的主题设计到敏感话题,,就想象出一副风景画面。\n我给你的主题可能是用中文描述,你给出的prompt只用英文。 接下来我每次发送给你的内容,都是一个主题,你需要结合以往所有的主体,请严格遵守以上规则返回我prompt。请务必输出英文,不能包含任何中文文字。\n",
11 | "sd_url": "https://xxxxxxx:8443/",
12 | "height": 720,
13 | "width": 1280,
14 | "sd_model_checkpoint": "AnythingV5_v5PrtRE",
15 | "sd_vae": "vae-ft-mse-840000-ema-pruned.safetensors",
16 | "add_prompt": "weapon,sword,long hair,armor,multiple boys,holding,shoulder armor,,",
17 | "negative_prompt": "easy_negative,badhandv4,nsfw, bottle,bad face, bad anatomy, bad proportions, bad perspective, multiple views, concept art, reference sheet, mutated hands and fingers, interlocked fingers, twisted fingers, excessively bent fingers, more than five fingers, lowres, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, artist name, low quality lowres multiple breasts, low quality lowres mutated hands and fingers, more than two arms, more than two hands, more than two legs, more than two feet, low quality lowres long body, low quality lowres mutation poorly drawn, low quality lowres black-white, low quality lowres bad anatomy, low quality lowres liquid body, low quality lowres liquid tongue, low quality lowres disfigured, low quality lowres malformed, low quality lowres mutated, low quality lowres anatomical nonsense, low quality lowres text font ui, low quality lowres error, low quality lowres malformed hands, low quality lowres long neck, low quality lowres blurred, low quality lowres lowers, low quality lowres low res, low quality lowres bad proportions, low quality lowres bad shadow, low quality lowres uncoordinated body, low quality lowres unnatural body, low quality lowres fused breasts, low quality lowres bad breasts, low quality lowres huge breasts, low quality lowres poorly drawn breasts, low quality lowres extra breasts, low quality lowres liquid breasts, low quality lowres heavy breasts, low quality lowres missing breasts, low quality lowres huge haunch, low quality lowres huge thighs, low quality lowres huge calf, low quality lowres bad hands, low quality lowres fused hand, low quality lowres missing hand, low quality lowres disappearing arms, low quality lowres disappearing thigh, low quality lowres disappearing calf, low quality lowres disappearing legs, low quality lowres fused ears, low quality lowres bad ears, low quality lowres poorly drawn ears, low quality lowres extra ears, low quality lowres liquid ears",
18 | "tutu_batch_cnt": 1,
19 | "seconds_per_tutu": 5,
20 | "sampler_index": "DPM++ 2M Karras",
21 | "watermark_image": "",
22 | "watermark_position": "topright",
23 | "fps": 25,
24 | "nouse": "",
25 | "add_prompt_bef": "(masterpiece:1.2),best quality,PIXIV,midjourney portrait,\n"
26 | }
--------------------------------------------------------------------------------
/pscripts/batch_tts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | import edge_tts
6 | import asyncio
7 | from concurrent.futures import TimeoutError
8 | from process_status import ProcessStatus
9 | from utils import vtt_to_json
10 | import json
11 | from log import logger
12 | import re
13 |
14 | # 读取配置文件
15 | with open('conf/config.json', 'r', encoding='utf-8') as file:
16 | config = json.load(file)
17 | work_dir = config['work_dir']
18 |
19 | batchCnt = config['tts_batch_cnt']
20 | scene_split_chars = config['scene_split_chars']
21 |
22 | pStatus = ProcessStatus()
23 |
24 |
25 | async def runTts(file_lock, sceneJsonObjs,sce,temp_dir,source_file_name, voice, rate, volume):
26 | # logger.info(f'{sce}')
27 | mp3_dir_path = f"{work_dir}/cache/{temp_dir}/mp3/{sce['seqnum']}"
28 | file_path_vtt = f"{mp3_dir_path}.vtt"
29 | file_path_mp3 = f"{mp3_dir_path}.mp3"
30 | TEXT = sce['text']
31 |
32 | communicate = edge_tts.Communicate(text=TEXT, voice=voice, rate=rate, volume=volume)
33 | #字幕
34 | submaker = edge_tts.SubMaker()
35 |
36 | with open(file_path_mp3, "wb") as file:
37 | async for chunk in communicate.stream():
38 | if chunk["type"] == "audio":
39 | file.write(chunk["data"])
40 | elif chunk["type"] == "WordBoundary":
41 | submaker.create_sub((chunk["offset"], chunk["duration"]), chunk["text"])
42 | zimuText = submaker.generate_subs();
43 | with open(file_path_vtt, "w", encoding="utf-8") as file:
44 | file.write(zimuText)
45 |
46 | #1解析字幕
47 | zimuArr = vtt_to_json(file_path_vtt)
48 | # 检查字幕最后一句话是否是小说的最后一句话,不是就是中间断了,重写跑一次
49 | #logger.info(f"TEXT:\n{TEXT}")
50 | #logger.info(f"vtt最后一句话:{zimuArr[-1]['text']}")
51 | lastZimuWord = zimuArr[-1]['text']
52 | lastZimuWord = lastZimuWord.replace(" ","")
53 | # 定义一个包含全角和半角标点符号以及空格的正则表达式
54 | punctuation_pattern = r'[^\u4e00-\u9fa5a-zA-Z0-9]'
55 | org_text = re.sub(punctuation_pattern, '', TEXT)
56 | last_index = org_text.rfind(lastZimuWord)
57 | logger.info(f"last_index:{last_index},lastZimuWord's Len:{len(lastZimuWord)},TEXT's Len:{len(org_text)}")
58 | text_len = len(org_text)
59 | lastZimuWord_len = len(lastZimuWord)
60 | dif = (text_len - (last_index + lastZimuWord_len)) / text_len
61 | logger.info(f"dif:{dif}\n")
62 | if dif > 0.015 and last_index > 0:
63 | await runTts(file_lock, sceneJsonObjs,sce,temp_dir,source_file_name, voice, rate, volume)
64 | return
65 |
66 | #vtt -> srt
67 | srtfile = f"{mp3_dir_path}.srt"
68 | with open(srtfile, 'w', encoding='utf-8') as f:
69 | for zimu in zimuArr:
70 | f.write(f"{zimu['seqnum']}\n")
71 | f.write(f"{zimu['start']} --> {zimu['end']}\n")
72 | text = zimu['text']
73 | text = text.replace(" ", "")
74 | f.write(f"{text}\n")
75 | f.write("\n")
76 |
77 | sce['start'] = zimuArr[0]['start']
78 | sce['end'] = zimuArr[-1]['end']
79 | sce['vttfile'] = file_path_vtt
80 | sce['srtfile'] = srtfile
81 | sce['mp3file'] = file_path_mp3
82 |
83 | async with file_lock:
84 | with open(f"{work_dir}/cache/{temp_dir}/{source_file_name}.sce", 'w', encoding='utf-8') as file:
85 | json.dump(sceneJsonObjs, file, ensure_ascii=False, indent=4)
86 |
87 | objs = sceneJsonObjs['objs']
88 | mp3_done_count = 0
89 | for i in range(0, len(objs)):
90 | s = objs[i]
91 | if s['mp3file']:
92 | mp3_done_count = mp3_done_count + 1
93 |
94 | jsonData = pStatus.get_json_data()
95 | jsonData['mp3_done_count'] = mp3_done_count
96 | pStatus.update(jsonData)
97 |
98 |
99 | async def main(argv):
100 | if len(argv) > 1:
101 | temp_dir = argv[1]
102 | pStatus.set_temp_dir(temp_dir)
103 | jsonData = pStatus.get_json_data()
104 |
105 | os.makedirs(f'{work_dir}/cache/{temp_dir}/mp3', exist_ok=True)
106 |
107 | # 读取场景文件
108 | with open(f"{jsonData['sce_file_path']}", 'r', encoding='utf-8') as file:
109 | sceneJsonObjs = json.load(file)
110 | toTtsSces = []
111 | for sce in sceneJsonObjs['objs']:
112 | #去除掉已经完成的
113 | if(not sce['mp3file']):
114 | toTtsSces.append(sce)
115 |
116 | # 将任务分批执行
117 | # logger.info(f"总共{len(toTtsFiles)}个分割文件,将分批进行处理...")
118 |
119 | voice = jsonData['role_name']
120 | rate = jsonData['rate']
121 | volume = jsonData['volume']
122 | pStatus.write_stage("tts_start",f"将分批处理语音,参数: 同时处理个数:{batchCnt},角色:{voice},音量:{volume},语速:{rate}")
123 |
124 | timeout_seconds = 30
125 | has_time_error = False
126 | file_lock = asyncio.Lock()
127 |
128 | for i in range(0, len(toTtsSces), batchCnt):
129 | # 获取当前批次的文件列表
130 | current_batch = toTtsSces[i:i + batchCnt]
131 | # 创建并发执行的任务
132 | #tasks = [runTts(file_lock, sceneJsonObjs,sce,temp_dir,jsonData["source_file_name"], voice, rate, volume) for sce in current_batch]
133 | # 并发执行当前批次的任务
134 | #await asyncio.gather(*tasks)
135 | tasks = []
136 | for sce in current_batch:
137 | # 使用 asyncio.wait_for 调用 runTts 并设置超时时间
138 | task = asyncio.wait_for(runTts(file_lock, sceneJsonObjs, sce, temp_dir, jsonData["source_file_name"], voice, rate, volume), timeout=timeout_seconds)
139 | tasks.append(task)
140 |
141 | # 并发执行当前批次的任务
142 | try:
143 | await asyncio.gather(*tasks)
144 |
145 | for sce in current_batch:
146 | logger.info(f"finished {sce['seqnum']}")
147 | except TimeoutError as e:
148 | logger.error(f"Timeout occurred for one or more tasks: {e}")
149 | # 这里可以处理超时的情况,例如重新尝试或跳过超时的任务
150 | has_time_error = True
151 | continue
152 | except Exception as e:
153 | logger.error(f"other error occured: {e}")
154 | # 这里可以处理超时的情况,例如重新尝试或跳过超时的任务
155 | has_time_error = True
156 | continue
157 |
158 | if has_time_error:
159 | await main(argv)
160 |
161 | pStatus.write_stage("tts_done",f"处理语音完成")
162 |
163 | else:
164 | logger.info("请提供一个文件路径作为参数。")
165 |
166 |
167 | if __name__ == "__main__":
168 | asyncio.run(main(sys.argv))
169 |
--------------------------------------------------------------------------------
/pscripts/gen_video.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | from process_status import ProcessStatus
6 | import json
7 | from chat_gpt import askChatgpt
8 | from chat_glm import askChatglm
9 | import subprocess
10 | import init_env
11 | from utils import vtt_to_json
12 | import time
13 | from log import logger
14 | import random
15 | from utils import get_json_data,time_str_to_seconds,duration
16 |
17 |
18 | # 读取配置文件
19 | with open('conf/config.json', 'r', encoding='utf-8') as file:
20 | config = json.load(file)
21 | work_dir = config['work_dir']
22 |
23 | pStatus = ProcessStatus()
24 |
25 |
26 |
27 |
28 | def create_video(sceObject, mp4_dir, sceObj):
29 | tutus = sceObj["tutu"]
30 | height = config['height']
31 | width = config['width']
32 | fps = config['fps']
33 |
34 | mp4_filename = f"{mp4_dir}/{sceObj['seqnum']}.mp4"
35 | ffmpeg_command = ['ffmpeg', '-y', '-f', 'image2pipe', '-r', str(fps), '-s', f'{width}x{height}']
36 | # 生成filter_complex参数
37 | filter_complex = ""
38 | concat_cmds = ""
39 | idx = 0
40 |
41 |
42 | for tu in tutus:
43 | duration = tu['duration']
44 | xiaoguos = []
45 | xiaoguos.append(f"[{idx}:v]scale=1200:-2,setsar=1:1[v{idx}];[v{idx}]crop=1200:670[v{idx}];[v{idx}]scale=8000:-1,zoompan=z='zoom+0.001':x=iw/2-(iw/zoom/2):y=ih/2-(ih/zoom/2):d={int(fps*duration)}:s={width}x{height}:fps={fps}[v{idx}];")
46 | xiaoguos.append(f"[{idx}:v]scale=1200:-2,setsar=1:1[v{idx}];[v{idx}]crop=1200:670[v{idx}];[v{idx}]scale=8000:-1,zoompan=z='1.1':x='if(lte(on,-1),(iw-iw/zoom)/2,x+2)':y='if(lte(on,1),(ih-ih/zoom)/2,y)':d={int(fps*duration)}:s={width}x{height}:fps={fps}[v{idx}];")
47 | xiaoguos.append(f"[{idx}:v]scale=1200:-2,setsar=1:1[v{idx}];[v{idx}]crop=1200:670[v{idx}];[v{idx}]scale=8000:-1,zoompan=z='1.1':x='if(lte(on,1),(iw-iw/zoom)/2,x-2)':y='if(lte(on,1),(ih-ih/zoom)/2,y)':d={int(fps*duration)}:s={width}x{height}:fps={fps}[v{idx}];")
48 | xiaoguos.append(f"[{idx}:v]scale=1200:-2,setsar=1:1[v{idx}];[v{idx}]crop=1200:670[v{idx}];[v{idx}]scale=8000:-1,zoompan=z='1.1':x='if(lte(on,1),(iw-iw/zoom)/2,x)':y='if(lte(on,1),(ih-ih/zoom)/2,y-2)':d={int(fps*duration)}:s={width}x{height}:fps={fps}[v{idx}];")
49 | xiaoguos.append(f"[{idx}:v]scale=1200:-2,setsar=1:1[v{idx}];[v{idx}]crop=1200:670[v{idx}];[v{idx}]scale=8000:-1,zoompan=z='1.1':x='if(lte(on,1),(iw-iw/zoom)/2,x)':y='if(lte(on,-1),(ih-ih/zoom)/2,y+2)':d={int(fps*duration)}:s={width}x{height}:fps={fps}[v{idx}];")
50 |
51 | rdIdx = random.randint(0, len(xiaoguos))
52 |
53 | filter_complex += xiaoguos[rdIdx - 1]
54 | concat_cmds += f"[v{idx}]"
55 | ffmpeg_command.extend(['-i', f"{tu['src']}"])
56 | idx +=1
57 | # 去掉最后一个分号
58 | concat_cmds +=f"concat=n={len(tutus)}:v=1[out]"
59 | filter_complex += concat_cmds
60 | ffmpeg_command.extend(['-loglevel', "quiet"]) #quiet
61 | # 添加filter_complex参数
62 | ffmpeg_command.extend(['-filter_complex', filter_complex])
63 | # 添加concat参数
64 | ffmpeg_command.extend(['-map', f'[out]'])
65 | ffmpeg_command.extend([ '-c:v', 'libx264', '-preset', 'slow', '-crf', '18', '-shortest', mp4_filename])
66 | # 使用subprocess运行ffmpeg命令
67 | ffmpeg_process = subprocess.Popen(ffmpeg_command, stdin=subprocess.PIPE)
68 | ffmpeg_process.stdin.close()
69 | ffmpeg_process.wait()
70 |
71 | logger.info(f"{mp4_filename}:视频创建完成")
72 | logger.info(f"{ffmpeg_command}")
73 |
74 | #添加音频和字幕
75 | # 构建 FFmpeg 命令
76 | # 判断_with_au.mp4文件是否存在,存在就删除
77 | video_path = f"{mp4_dir}/{sceObj['seqnum']}_with_au.mp4"
78 | if os.path.exists(video_path):
79 | os.remove(video_path)
80 |
81 | watermark_image = config['watermark_image'] # 水印图片的路径
82 | watermark_position = config['watermark_position'] # 水印位置,可以是topleft, topright
83 | srtfilename = sceObj['srtfile']
84 | srtfilename = srtfilename.replace("\\","/")
85 | srtfilename = srtfilename.replace(":","\:")
86 | if watermark_image and watermark_position:
87 | posX = 10
88 | posY = 10
89 | if watermark_position == "topright":
90 | posX = "main_w-200"
91 | posY = 10
92 |
93 | ffmpeg_cmd = [
94 | "ffmpeg",
95 | "-loglevel", "quiet", # 隐藏进度信息 quiet
96 | "-i", mp4_filename, # 输入视频
97 | "-i", f"{sceObj['mp3file']}", # 输入音频
98 | "-i", watermark_image, # 水印图片路径
99 | "-filter_complex", f"overlay=x={posX}:y={posY},subtitles='{srtfilename}'", # 添加水印,并将字幕叠加在水印上
100 | "-c:a", "aac", # 音频编码:转换为 AAC
101 | "-strict", "-2", # 部分 FFmpeg 版本需要这个选项来启用某些编码器
102 | video_path # 输出文件
103 | ]
104 | else:
105 | ffmpeg_cmd = [
106 | "ffmpeg",
107 | "-loglevel", "quiet", # 隐藏进度信息 quiet
108 | "-i", mp4_filename, # 输入视频
109 | "-i", f"{sceObj['mp3file']}", # 输入音频
110 | "-filter_complex", f"subtitles='{srtfilename}'", # 添加水印
111 | "-c:a", "aac", # 音频编码:转换为 AAC
112 | "-strict", "-2", # 部分 FFmpeg 版本需要这个选项来启用某些编码器
113 | video_path # 输出文件
114 | ]
115 | # 执行 FFmpeg 命令
116 | logger.info(f"ffmpeg command:{ffmpeg_cmd}")
117 | logger.info(f"{sceObj['seqnum']}:FFmpeg开始:" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
118 | subprocess.run(ffmpeg_cmd)
119 | logger.info(f"{sceObj['seqnum']}:FFmpeg结束:" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
120 | sceObj['mp4file'] = mp4_filename
121 | sceObj['mp4aufile'] = video_path
122 |
123 | jsonData = pStatus.get_json_data()
124 | with open(jsonData['sce_file_path'], 'w', encoding='utf-8') as file:
125 | json.dump(sceObject, file, ensure_ascii=False, indent=4)
126 |
127 | objs = sceObject['objs']
128 | mp4_done_count = 0
129 | for i in range(0, len(objs)):
130 | s = objs[i]
131 | if s['mp4aufile']:
132 | mp4_done_count = mp4_done_count + 1
133 |
134 | jsonData['mp4_done_count'] = mp4_done_count
135 | pStatus.update(jsonData)
136 |
137 |
138 | def main(argv):
139 | if len(argv) > 1:
140 | temp_dir = argv[1]
141 | pStatus.set_temp_dir(temp_dir)
142 | jsonData = pStatus.get_json_data()
143 | sce_file_path = jsonData["sce_file_path"]
144 | sceObject = get_json_data(sce_file_path)
145 | toVideoFiles = []
146 | for sf in sceObject['objs']:
147 | if(not sf['mp4aufile']):
148 | toVideoFiles.append(sf)
149 | pStatus.write_stage("video_start",f"将进行合成视频")
150 |
151 | mp4_dir = f'{work_dir}/cache/{temp_dir}/mp4'
152 | if not os.path.exists(mp4_dir):
153 | os.mkdir(mp4_dir)
154 | for sceObj in toVideoFiles:
155 | create_video(sceObject, mp4_dir, sceObj)
156 |
157 | pStatus.write_stage("video_done",f"合成视频完成")
158 | else:
159 | logger.info("请提供一个文件路径作为参数。")
160 |
161 |
162 | if __name__ == "__main__":
163 | main(sys.argv)
164 |
--------------------------------------------------------------------------------
/pscripts/ai_tutu.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # author:laure
3 | import sys
4 | import os
5 | import asyncio
6 | from concurrent.futures import TimeoutError
7 | from process_status import ProcessStatus
8 | import json
9 | from chat_gpt import askChatgpt
10 | from chat_glm import askChatglm
11 | from sd_api import doOnePrompt,doPromptBatch
12 | import time
13 | import traceback
14 | from utils import get_json_data,time_str_to_seconds,duration
15 | from log import logger
16 | # 读取配置文件
17 | with open('conf/config.json', 'r', encoding='utf-8') as file:
18 | config = json.load(file)
19 | work_dir = config['work_dir']
20 | batchCnt = config['tutu_batch_cnt']
21 |
22 | pStatus = ProcessStatus()
23 |
24 |
25 | async def runAiTutu(sceneJsonObjs,sce,temp_dir,source_file_name):
26 | seconds_per_tutu = config['seconds_per_tutu']
27 | #总时长
28 | drt = duration("00:00:00.000",sce['end'])
29 | tutus = []
30 | if seconds_per_tutu == 0 or seconds_per_tutu >= drt:
31 | tutus.append({
32 | "id":0,
33 | "duration":drt,
34 | "src":""
35 | })
36 | else:
37 | #计算多少张图片和持续时间
38 | cnt = int(drt / seconds_per_tutu)
39 | mode = round(drt % seconds_per_tutu,3)
40 | if mode > 0:
41 | for i in range(0, cnt):
42 | tutus.append({
43 | "id":i,
44 | "duration":seconds_per_tutu,
45 | "src":""
46 | })
47 | tutus.append({
48 | "id":cnt,
49 | "duration":mode,
50 | "src":""
51 | })
52 | else:
53 | for i in range(0, cnt):
54 | tutus.append({
55 | "id":i,
56 | "duration":seconds_per_tutu,
57 | "src":""
58 | })
59 |
60 | prompt = sce['prompt']
61 | seqnum = sce['seqnum']
62 | if prompt:
63 | if config['add_prompt_bef']:
64 | prompt = f"{config['add_prompt_bef']},{prompt}"
65 | if config['add_prompt']:
66 | prompt = f"{prompt},{config['add_prompt']}"
67 |
68 |
69 | #开始生图
70 | img_dir = f"{work_dir}/cache/{temp_dir}/images"
71 | img_path = f"{img_dir}/{seqnum}_{int(time.time())}"
72 | imgpaths = doPromptBatch(img_dir,img_path, prompt, len(tutus))
73 | for i, tutuObj in enumerate(tutus, 1):
74 | tutuObj["src"] = imgpaths[i-1]
75 |
76 | if len(tutus) > 0:
77 | sce['tutu'].extend(tutus)
78 | #logger.info(f"{sce}")
79 | #logger.info(f"{tutus}")
80 |
81 | with open(f"{work_dir}/cache/{temp_dir}/{source_file_name}.sce", 'w', encoding='utf-8') as file:
82 | json.dump(sceneJsonObjs, file, ensure_ascii=False, indent=4)
83 |
84 | objs = sceneJsonObjs['objs']
85 | tutu_done_count = 0
86 | for i in range(0, len(objs)):
87 | s = objs[i]
88 | if len(s['tutu']) > 0:
89 | tutu_done_count = tutu_done_count + 1
90 |
91 | jsonData = pStatus.get_json_data()
92 | jsonData['tutu_done_count'] = tutu_done_count
93 | pStatus.update(jsonData)
94 |
95 |
96 | async def regenTutu(temp_dir,seqnum,id):
97 | pStatus.set_temp_dir(temp_dir)
98 | statusData = pStatus.get_json_data()
99 | sceneJsonObjs = get_json_data(statusData['sce_file_path'])
100 | objs = sceneJsonObjs["objs"]
101 | sce = None
102 | for obj in objs:
103 | if obj["seqnum"] == seqnum:
104 | sce = obj
105 |
106 | if sce == None:
107 | return
108 |
109 | tutus = sce['tutu']
110 | if len(tutus) == 0:
111 | return
112 |
113 | toChangeTu = None
114 | for tu in tutus:
115 | if tu['id'] == id:
116 | toChangeTu = tu
117 |
118 | if toChangeTu == None:
119 | return
120 |
121 | #开始生图
122 | prompt = sce['prompt']
123 | if prompt:
124 | if config['add_prompt_bef']:
125 | prompt = f"{config['add_prompt_bef']},{prompt}"
126 | if config['add_prompt']:
127 | prompt = f"{prompt},{config['add_prompt']}"
128 |
129 | img_dir = f"{work_dir}/cache/{temp_dir}/images"
130 | img_path = f"{img_dir}/{seqnum}_{id}_{int(time.time())}"
131 | img_path = doOnePrompt(img_dir,img_path, prompt)
132 | toChangeTu["src"] = img_path
133 |
134 | with open(f"{work_dir}/cache/{temp_dir}/{statusData['source_file_name']}.sce", 'w', encoding='utf-8') as file:
135 | json.dump(sceneJsonObjs, file, ensure_ascii=False, indent=4)
136 |
137 |
138 |
139 |
140 |
141 |
142 | async def main(argv):
143 | if len(argv) > 3:
144 | action = argv[1]
145 | if action == "all":
146 | temp_dir = argv[2]
147 | pStatus.set_temp_dir(temp_dir)
148 | jsonData = pStatus.get_json_data()
149 | # 读取场景文件
150 | with open(f"{jsonData['sce_file_path']}", 'r', encoding='utf-8') as file:
151 | sceneJsonObjs = json.load(file)
152 | toTutuSces = []
153 | for sce in sceneJsonObjs['objs']:
154 | #去除掉已经完成的
155 | if(len(sce['tutu']) == 0):
156 | toTutuSces.append(sce)
157 |
158 | pStatus.write_stage("aitutu_start",f"将分批进行生图,参数: 同时处理个数:{batchCnt}")
159 | timeout_seconds = 60
160 | has_time_error = False
161 |
162 |
163 | for i in range(0, len(toTutuSces), batchCnt):
164 | # 获取当前批次的文件列表
165 | current_batch = toTutuSces[i:i + batchCnt]
166 | # 创建并发执行的任务
167 | #tasks = [runAiTutu(sceneJsonObjs,sce,temp_dir,jsonData["source_file_name"]) for sce in current_batch]
168 | # 并发执行当前批次的任务
169 | #await asyncio.gather(*tasks)
170 | tasks = []
171 | for sce in current_batch:
172 | # 使用 asyncio.wait_for 调用 runTts 并设置超时时间
173 | task = asyncio.wait_for(runAiTutu(sceneJsonObjs,sce,temp_dir,jsonData["source_file_name"]), timeout=timeout_seconds)
174 | tasks.append(task)
175 |
176 | try:
177 | await asyncio.gather(*tasks)
178 | for sf in current_batch:
179 | logger.info(f"finished {sf['seqnum']}")
180 | except TimeoutError as e:
181 | logger.error(f"Timeout occurred for one or more tasks: {e}")
182 | # 这里可以处理超时的情况,例如重新尝试或跳过超时的任务
183 | has_time_error = True
184 | continue
185 | except Exception as e:
186 | logger.error(f"other error occured: {e}")
187 | logger.error(traceback.format_exc())
188 | # 这里可以处理超时的情况,例如重新尝试或跳过超时的任务
189 | has_time_error = True
190 | continue
191 |
192 | if has_time_error:
193 | await main(argv)
194 | pStatus.write_stage("aitutu_done",f"生图完成")
195 | else:
196 | temp_dir = argv[2]
197 | argv3 = (argv[3])
198 | logger.info(f"regen tutu:{temp_dir},{argv3}")
199 | params = argv3.split(":")
200 | if len(params) == 2:
201 | seqnum = int(params[0])
202 | id = int(params[1])
203 | await regenTutu(temp_dir,seqnum,id)
204 | else:
205 | logger.info("请提供一个文件路径作为参数。")
206 |
207 |
208 | if __name__ == "__main__":
209 | asyncio.run(main(sys.argv))
210 |
--------------------------------------------------------------------------------
/src/pages/Setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
221 |
222 |
223 |
224 |
331 |
332 |
--------------------------------------------------------------------------------
/electron/ipc_actions.js:
--------------------------------------------------------------------------------
1 |
2 | const { ipcMain,BrowserWindow } = require('electron');
3 | const { spawn } = require('child_process');
4 | const fs = require('fs');
5 | const path = require('path');
6 | const winston = require('winston');
7 | const { exec } = require('child_process');
8 | const customFormat = winston.format.printf(({ timestamp, level, message, label, ...rest }) => {
9 | return `${timestamp} ${level}: ${message}`;
10 | });
11 |
12 | function getRelateFileConfigPath(filename){
13 | if(fs.existsSync("./pscripts/init_env.py")){
14 | //dev
15 | return `../${filename}`;
16 | }else{
17 | //release
18 | return `../../../${filename}`;
19 | }
20 | }
21 |
22 | let logger = null;
23 | // 创建一个日志记录器
24 | function initLogger(){
25 | let config = getJson(getRelateFileConfigPath("conf/config.json"));
26 | let work_dir = config.work_dir;
27 | logger = winston.createLogger({
28 | level: 'info', // 设置日志级别,可以是 'error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'
29 | format: winston.format.combine(
30 | winston.format.timestamp(), // 添加时间戳
31 | winston.format.errors({ stack: true }), // 包含错误堆栈(可选)
32 | customFormat // 自定义格式化输出
33 | ),
34 | transports: [
35 | new winston.transports.File({ filename: work_dir + '/logs/ipc.log' }), // 将日志写入文件
36 | new winston.transports.Console({
37 | format: winston.format.combine(
38 | winston.format.colorize(), // 可选,为控制台输出添加颜色
39 | customFormat // 使用自定义格式化程序,包含时间戳
40 | ),
41 | }),
42 | ],
43 | });
44 | }
45 | /**
46 | *
47 | * @param {*} scriptName 放在pscripts目录下的python脚本名字
48 | * @param {*} params 传递给脚本的参数数组
49 | */
50 | function runScript(scriptName,params){
51 | if(!!!logger) initLogger();
52 | //检查是不是发行版本,发行版本运行exe,开发版本运行.py
53 | logger.info("run script:" + scriptName);
54 | if(fs.existsSync("./pscripts/init_env.py")){
55 | logger.info("dev env");
56 | const pythonExecutable = path.join(__dirname, '../py/Scripts/python.exe');
57 | const scriptPath = path.join(__dirname, '../pscripts/' + scriptName +".py");
58 | params.unshift(scriptPath);
59 | return spawn(pythonExecutable, params);
60 | }else{
61 | logger.info("package env");
62 | const exePath = path.join(__dirname, '../lexe/' + scriptName +".exe");
63 | return spawn(exePath, params);
64 | }
65 | }
66 |
67 | function getJson(filename) {
68 | let filepath = path.join(__dirname, filename);
69 | try {
70 | const data = fs.readFileSync(filepath, 'utf8');
71 | const jsonData = JSON.parse(data);
72 | return jsonData;
73 | } catch (error) {
74 | console.error('Error reading or parsing JSON file:', error);
75 | console.error("读取文件失败:" + filepath)
76 | throw error;
77 | }
78 | };
79 |
80 | function getJsonAbsFile(filename) {
81 | try {
82 | const data = fs.readFileSync(filename, 'utf8');
83 | const jsonData = JSON.parse(data);
84 | return jsonData;
85 | } catch (error) {
86 | console.error('Error reading or parsing JSON file:', error);
87 | console.error("读取文件失败:" + filename)
88 | throw error;
89 | }
90 | };
91 |
92 | function addStaticOptions(options){
93 | let rates = ["-10%","-20%","-30%","-40%","-50%","-100%","+0%","+10%","+20%","+30%","+40%","+50%","+100%"];
94 | let volumes = ["-10%","-20%","-30%","-40%","-50%","-100%","+0%","+10%","+20%","+30%","+40%","+50%","+100%"];
95 | let watermark_position_list = ["topleft","topright"];
96 | options.rates = rates;
97 | options.volumes = volumes;
98 | options.watermark_position_list = watermark_position_list;
99 | }
100 |
101 | function getNewStatus(temp_dir){
102 | let config = getJson(getRelateFileConfigPath("conf/config.json"));
103 | let work_dir = config.work_dir;
104 | let filePath = `${work_dir}/cache/${temp_dir}/status.json`;
105 | let rslt = true;
106 | let status = null;
107 | try{
108 | status = getJsonAbsFile(filePath);
109 | //增加各场景的参数
110 | const sceData = getJsonAbsFile(status.sce_file_path);
111 | status.sces = sceData.objs;
112 | }catch(error){
113 | logger.error("error:" + error);
114 | rslt = false;
115 | }
116 | status.work_dir = work_dir;
117 | return {
118 | rslt,
119 | status
120 | }
121 | }
122 |
123 |
124 | function setupIpcActions() {
125 | ipcMain.on('to-open-setting-page', (event) => {
126 | event.reply('open-setting-page', {
127 | rlst:0
128 | });
129 | });
130 | ipcMain.on('reload-app', (event) => {
131 | if (BrowserWindow.getFocusedWindow()) {
132 | BrowserWindow.getFocusedWindow().reload();
133 | }
134 | });
135 | ipcMain.on('get-config', (event) => {
136 | let config = getJson(getRelateFileConfigPath("conf/config.json"));
137 | let options = getJson(getRelateFileConfigPath("conf/options.json"));
138 | addStaticOptions(options);
139 | event.reply('get-config-done', {
140 | config,
141 | options
142 | });
143 | });
144 | ipcMain.on('get-options', (event) => {
145 | let options = getJson(getRelateFileConfigPath("conf/options.json"));
146 | addStaticOptions(options);
147 | event.reply('get-options-done', {
148 | options
149 | });
150 | });
151 |
152 | ipcMain.on('refresh-options', (event) => {
153 | if(!!!logger) initLogger();
154 | logger.info("ipc run refresh-options");
155 | const script = runScript("refresh_options",[]);
156 |
157 | script.stdout.on('data', (data) => {
158 | logger.info(`stdout: ${data}`);
159 | });
160 |
161 | script.stderr.on('data', (data) => {
162 | logger.error(`stderr: ${data}`);
163 | });
164 |
165 | script.on('close', (code) => {
166 | logger.info(`子进程退出,退出码 ${code}`);
167 | let options = getJson(getRelateFileConfigPath("conf/options.json"));
168 | addStaticOptions(options);
169 | event.reply('refresh-options-done', {
170 | code,
171 | options
172 | });
173 | });
174 | });
175 |
176 |
177 | ipcMain.on('save-config', async (event,config) => {
178 | let rslt = true;
179 | const jsonString = JSON.stringify(config, null, 2); // 第二个参数是缩进,用于美化输出,这里设置为2
180 |
181 | // 将字符串写入文件
182 | let filepath = path.join(__dirname, getRelateFileConfigPath("conf/config.json"));
183 | fs.writeFile(filepath, jsonString, 'utf8', (err) => {
184 | if (err) {
185 | console.error('Error writing file:', err);
186 | rslt = false;
187 | }
188 | event.reply('save-config-done', {
189 | rslt
190 | });
191 | });
192 | });
193 |
194 | ipcMain.on('get-temp-dirs', (event) => {
195 | let config = getJson(getRelateFileConfigPath("conf/config.json"));
196 | let work_dir = config.work_dir;
197 | // 使用 fs.readdirSync() 获取目录下的所有文件和子目录
198 | directoryPath = work_dir + "/cache";
199 | const items = fs.readdirSync(directoryPath);
200 | const subdirectories = [];
201 | // 遍历每个项目,检查是否为目录
202 | items.forEach(item => {
203 | const itemPath = path.join(directoryPath, item);
204 | // 使用 fs.statSync() 获取项目的状态
205 | const itemStats = fs.statSync(itemPath);
206 | if (itemStats.isDirectory()) {
207 | subdirectories.push(item);
208 | }
209 | });
210 | event.reply('get-temp-dirs-done', {
211 | dirs:subdirectories
212 | });
213 | });
214 |
215 | ipcMain.on('get-continue-status', (event, temp_dir) => {
216 | let config = getJson(getRelateFileConfigPath("conf/config.json"));
217 | let work_dir = config.work_dir;
218 | // 使用 fs.readdirSync() 获取目录下的所有文件和子目录
219 | let filePath = `${work_dir}/cache/${temp_dir}/status.json`;
220 | let status = getJsonAbsFile(filePath);
221 | event.reply('get-continue-status-done', {
222 | status
223 | });
224 | });
225 |
226 | ipcMain.on('refresh-status', (event, temp_dir) => {
227 | rslt = getNewStatus(temp_dir);
228 | event.reply('refresh-status-done', rslt);
229 | });
230 |
231 | ipcMain.on('run-split', (event, data) => {
232 | if(!!!logger) initLogger();
233 | let {newFilePath, temp_dir,role_name,rate,volume} = data;
234 | logger.info("newFilePath:"+newFilePath)
235 | let filePath = newFilePath;
236 | logger.info("ipc run run-split");
237 | logger.info(filePath);
238 | const script = runScript("split_file", [filePath,temp_dir,role_name,rate,volume]);
239 |
240 | script.stdout.on('data', (data) => {
241 | logger.info(`stdout: ${data}`);
242 | });
243 |
244 | script.stderr.on('data', (data) => {
245 | logger.error(`stderr: ${data}`);
246 | });
247 |
248 | script.on('close', (code) => {
249 | logger.info(`子进程退出,退出码 ${code}`);
250 | event.reply('split-done', {
251 | code,
252 | filePath,
253 | temp_dir
254 | });
255 | });
256 | });
257 |
258 |
259 | ipcMain.on('run-batch-tts', (event, data) => {
260 | let temp_dir = data.temp_dir;
261 | if(!!!logger) initLogger();
262 | logger.info("ipc run run-batch-tts");
263 | logger.info(temp_dir);
264 |
265 | const script = runScript("batch_tts", [temp_dir]);
266 |
267 | script.stdout.on('data', (data) => {
268 | logger.info(`stdout: ${data}`);
269 | });
270 |
271 | script.stderr.on('data', (data) => {
272 | logger.error(`stderr: ${data}`);
273 | });
274 |
275 | script.on('close', (code) => {
276 | logger.info(`子进程退出,退出码 ${code}`);
277 | rslt = getNewStatus(temp_dir);
278 | event.reply('run-batch-tts-done', {
279 | code,
280 | rslt
281 | });
282 | });
283 | });
284 |
285 | ipcMain.on('run-ai-prompt', (event, data) => {
286 | let temp_dir = data.temp_dir;
287 | if(!!!logger) initLogger();
288 | logger.info("ipc run run-ai-prompt")
289 | logger.info(temp_dir)
290 |
291 | const script = runScript("ai_prompt", ["all",temp_dir,""]);
292 |
293 | script.stdout.on('data', (data) => {
294 | logger.info(`stdout: ${data}`);
295 | });
296 |
297 | script.stderr.on('data', (data) => {
298 | logger.error(`stderr: ${data}`);
299 | });
300 |
301 | script.on('close', (code) => {
302 | logger.info(`子进程退出,退出码 ${code}`);
303 | rslt = getNewStatus(temp_dir);
304 | event.reply('run-ai-prompt-done', {
305 | code,
306 | rslt
307 | });
308 | });
309 | });
310 |
311 | ipcMain.on('run-prompt-save', (event, data) => {
312 | let sce_file_path = data.sce_file_path;
313 | let sces = data.sces;
314 | let rootObject = {
315 | objs:sces
316 | };
317 | if(!!!logger) initLogger();
318 | logger.info("ipc run run-prompt-save")
319 |
320 | const jsonString = JSON.stringify(rootObject, null, 2); // 第二个参数是缩进,用于美化输出,这里设置为2
321 |
322 | try {
323 | fs.writeFileSync(sce_file_path, jsonString, 'utf8');
324 | rslt = getNewStatus(data.temp_dir);
325 | event.reply('run-prompt-save-done', {
326 | code:0,
327 | rslt
328 | });
329 | } catch (err) {
330 | logger.info("error occured:" + err)
331 | event.reply('run-prompt-save-done', {
332 | code:1,
333 | });
334 | }
335 | });
336 |
337 | ipcMain.on('run-regen-prompt', (event, data) => {
338 | if(!!!logger) initLogger();
339 | logger.info("ipc run run-regen-prompt")
340 | let temp_dir = data.status.temp_dir;
341 | let seqnum = data.sce.seqnum;
342 | logger.info(seqnum)
343 |
344 | const script = runScript("ai_prompt", ["regen",temp_dir,seqnum]);
345 | script.stdout.on('data', (data) => {
346 | // logger.info(`stdout: ${data}`);
347 | });
348 |
349 | script.stderr.on('data', (data) => {
350 | // logger.error(`stderr: ${data}`);
351 | });
352 |
353 | script.on('close', (code) => {
354 | logger.info(`子进程退出,退出码 ${code}`);
355 | rslt = getNewStatus(temp_dir);
356 | event.reply('run-regen-prompt-done', {
357 | code,
358 | rslt,
359 | seqnum,
360 | });
361 | });
362 | });
363 |
364 |
365 | ipcMain.on('run-ai-tutu', (event, data) => {
366 | let temp_dir = data.temp_dir;
367 | if(!!!logger) initLogger();
368 | logger.info("ipc run run-ai-tutu")
369 | logger.info(temp_dir)
370 |
371 | const script = runScript("ai_tutu", ["all",temp_dir,""]);
372 |
373 | script.stdout.on('data', (data) => {
374 | logger.info(`stdout: ${data}`);
375 | });
376 |
377 | script.stderr.on('data', (data) => {
378 | logger.error(`stderr: ${data}`);
379 | });
380 |
381 | script.on('close', (code) => {
382 | logger.info(`子进程退出,退出码 ${code}`);
383 | rslt = getNewStatus(temp_dir);
384 | event.reply('run-ai-tutu-done', {
385 | code,
386 | rslt
387 | });
388 | });
389 | });
390 |
391 | ipcMain.on('run-regen-tutu', (event, data) => {
392 | if(!!!logger) initLogger();
393 | logger.info("ipc run run-regen-tutu")
394 | let temp_dir = data.status.temp_dir;
395 | let tutuid = data.tutuid;
396 | logger.info(tutuid)
397 |
398 | const script = runScript("ai_tutu", ["regen",temp_dir,tutuid]);
399 | script.stdout.on('data', (data) => {
400 | logger.info(`stdout: ${data}`);
401 | });
402 |
403 | script.stderr.on('data', (data) => {
404 | logger.error(`stderr: ${data}`);
405 | });
406 |
407 | script.on('close', (code) => {
408 | rslt = getNewStatus(temp_dir);
409 | event.reply('run-regen-tutu-done', {
410 | code,
411 | rslt,
412 | tutuid
413 | });
414 | });
415 | });
416 |
417 |
418 | ipcMain.on('run-gen-video', (event, data) => {
419 | if(!!!logger) initLogger();
420 | let temp_dir = data.temp_dir;
421 | logger.info("ipc run run-gen-video")
422 | logger.info(temp_dir)
423 |
424 | const script = runScript("gen_video", [temp_dir]);
425 |
426 | script.stdout.on('data', (data) => {
427 | logger.info(`stdout: ${data}`);
428 | });
429 |
430 | script.stderr.on('data', (data) => {
431 | logger.error(`stderr: ${data}`);
432 | });
433 |
434 | script.on('close', (code) => {
435 | logger.info(`子进程退出,退出码 ${code}`);
436 | rslt = getNewStatus(temp_dir);
437 | event.reply('run-gen-video-done', {
438 | code,
439 | rslt,
440 | temp_dir
441 | });
442 | });
443 | });
444 |
445 |
446 |
447 | ipcMain.on('run-merge-mp4', (event, data) => {
448 | if(!!!logger) initLogger();
449 | let temp_dir = data.temp_dir;
450 | logger.info("ipc run run-merge-mp4")
451 | logger.info(temp_dir)
452 | const script = runScript("merge_mp4", [temp_dir]);
453 |
454 | script.stdout.on('data', (data) => {
455 | logger.info(`stdout: ${data}`);
456 | });
457 |
458 | script.stderr.on('data', (data) => {
459 | logger.error(`stderr: ${data}`);
460 | });
461 |
462 | script.on('close', (code) => {
463 | logger.info(`子进程退出,退出码 ${code}`);
464 | rslt = getNewStatus(temp_dir);
465 | event.reply('run-merge-mp4-done', {
466 | code,
467 | rslt,
468 | temp_dir
469 | });
470 | });
471 | });
472 | // 打开目录的 IPC 消息处理函数
473 | ipcMain.on('open-directory', (event, directoryPath) => {
474 | if(!!!logger) initLogger();
475 | logger.info(`open directory: ${directoryPath}`);
476 | const os = require('os');
477 | let command;
478 | if (os.platform() === 'win32') {
479 | command = `explorer "${directoryPath}"`; // Windows 命令
480 | } else if (os.platform() === 'darwin') {
481 | command = `open -R "${directoryPath}"`; // macOS 命令
482 | } else {
483 | command = `xdg-open "${directoryPath}"`; // 其他 Linux 发行版可能使用 xdg-open
484 | }
485 | exec(command);
486 | });
487 |
488 | };
489 |
490 | module.exports = { setupIpcActions };
--------------------------------------------------------------------------------
/src/pages/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | LPANDA
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
112 |
113 |
114 |
115 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
179 |
180 |
181 |
182 |
183 | {{ status.current_stage_descr }}
184 | 当前进度:{{ status.mp3_done_count }} / {{ status.sce_count }}
185 |
186 |
187 |
188 |
189 |
190 |
192 |
194 |
195 |
196 |
197 |
198 |
199 | {{ status.current_stage_descr }}
200 | 当前进度:{{ status.gpt_done_count }} / {{ status.sce_count }}
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | 重新生成中
209 | 队列任务数量:{{ taskQueue.length() }}
210 |
211 |
212 |
213 |
215 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | {{ status.current_stage_descr }}
226 | 当前进度:{{ status.tutu_done_count }} / {{ status.sce_count }}
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | 重新生成中
235 | 队列任务数量:{{ taskQueue.length() }}
236 |
237 |
238 |
239 |
241 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
![]()
256 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | {{ status.current_stage_descr }}
268 | 当前进度:{{ status.mp4_done_count }} / {{ status.sce_count }}
269 |
270 |
271 |
272 |
273 |
274 |
275 |
277 |
279 |
280 |
281 |
288 |
289 |
290 |
291 |
292 |
293 | {{ status.current_stage_descr }}
294 |
295 |
296 |
297 |
299 |
301 |
302 |
303 |
304 |
306 |
310 |
311 |
312 | 合并中...
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
342 |
343 |
344 |
345 |
346 |
364 |
365 |
366 |
367 |
368 |
370 |
371 |
500 |
--------------------------------------------------------------------------------
/src/pages/App.js:
--------------------------------------------------------------------------------
1 |
2 | import {Message } from "view-design";
3 | import Utils from '@/../libs/utils'
4 | import initQueue from '@/../libs/task_queue'
5 | export default {
6 | name:"app",
7 | data(){
8 | let defaultStatus = {
9 | temp_dir:"",
10 | actionType:"new",
11 | newFilePath:"",
12 | continueDir:"",
13 | role_name:"zh-CN-YunxiNeural",
14 | rate:"+10%",
15 | volume:"+0%",
16 | source_file_name: "",
17 | source_file_path: "",
18 | sce_file_path: "",
19 | create_dttm: "",
20 | end_dttm: "",
21 | scene_split_chars: 0,
22 | current_stage: "",
23 | current_stage_descr: ",参数: 同时处理个数:10,角色:zh-CN-YunxiNeural,音量:+0%,语速:+10%",
24 | sce_count: 0,
25 | mp3_done_count: 0,
26 | gpt_done_count: 0,
27 | tutu_done_count: 0,
28 | mp4_done_count: 0,
29 | seconds_per_tutu: 0,
30 | work_dir:"",
31 | };
32 | let status = {};
33 | Utils.copyValues(defaultStatus, status);
34 | return {
35 | step:0,
36 | stepNames:['开始','转语音','推理提示词','AI生图','合成视频','输出视频'],
37 | refreshTimer:null,
38 | defaultStatus:defaultStatus,
39 | status:status,
40 | continueList:[],
41 | ruleInline0:{
42 |
43 | },
44 | options:{
45 |
46 | },
47 | loading:{
48 | next0:false,
49 | next1:false,
50 | next2:false,
51 | next3:false,
52 | next4:false,
53 | next5:false,
54 | next6:false,
55 | prompt_save:false,
56 | prompt_regen:false,
57 | tutu_regen:false,
58 | },
59 | sces:[],
60 | aicolumns:[
61 | {
62 | title: '序号',
63 | key: 'seqnum',
64 | width:150,
65 | },
66 | {
67 | title: '场景内容',
68 | key: 'text',
69 | render: (h, params) => {
70 | let text = params.row.text;
71 | if(text.length > 100){
72 | text = text.substring(0,100) + "...";
73 | }
74 | let vue = this;
75 | return h('a', {
76 | class:"column-text",
77 | props: {},
78 | on:{
79 | click:()=>{
80 | vue.openTextModal(params.row)
81 | }
82 | },
83 | },
84 | [
85 | h('span', text)
86 | ]);
87 | }
88 | },
89 | {
90 | title: '提示词',
91 | key: 'prompt',
92 | render: (h, params) => {
93 | let prompt = params.row.prompt;
94 | if(prompt.length > 200){
95 | prompt = prompt.substring(0,200) + "...";
96 | }
97 | let vue = this;
98 | return h('a', {
99 | class:"column-prompt",
100 | props: {},
101 | on:{
102 | click:()=>{
103 | vue.openPromptModal(params.row)
104 | }
105 | },
106 | },
107 | [
108 | h('span', prompt)
109 | ]);
110 | }
111 | },
112 | {
113 | title: '重新推理',
114 | key: 'actions',
115 | width:80,
116 | render: (h, params) => {
117 | let vue = this;
118 | if(vue.status.current_stage == "aiprompt_done"){
119 | return h('Button', {
120 | class:"column-regen-prompt",
121 | props: {
122 | type:"info",
123 | icon:"md-refresh",
124 | shape:"circle",
125 | loading:vue.redoArr.includes(`p_${params.row.seqnum}`),
126 | },
127 | on:{
128 | click:()=>{
129 | vue.doRegenPrompt(this,params.row)
130 | }
131 | },
132 | });
133 | }else{
134 | return h("div","");
135 | }
136 | }
137 | },],
138 | aitableHeight:200,
139 | promptModal:{
140 | show:false,
141 | row:{},
142 | readonly:false,
143 | },
144 | tutuModal:{
145 | show:false,
146 | row:{},
147 | tutu:{},
148 | },
149 | taskQueue:null,
150 | redoArr: [],
151 | tutuMode: "table",
152 | tutucolumns:[
153 | {
154 | title: '序号',
155 | key: 'seqnum',
156 | width:150,
157 | },
158 | {
159 | title: '场景内容',
160 | key: 'text',
161 | render: (h, params) => {
162 | let text = params.row.text;
163 | if(text.length > 100){
164 | text = text.substring(0,100) + "...";
165 | }
166 | let vue = this;
167 | return h('a', {
168 | class:"column-text",
169 | props: {},
170 | on:{
171 | click:()=>{
172 | vue.openTutuTextModal(params.row)
173 | }
174 | },
175 | },
176 | [
177 | h('span', text)
178 | ]);
179 | }
180 | },
181 | {
182 | title: '图图们',
183 | key: 'tutus',
184 | render: (h, params) => {
185 | let vue = this;
186 | let tutuArr = [];
187 | for(let tutu of params.row.tutu){
188 | let subs = [h("img",{
189 | attrs:{
190 | src:tutu.src
191 | },
192 | on:{
193 | click:()=>{
194 | vue.tutuModalShow(this,params.row,tutu);
195 | }
196 | }
197 | })];
198 | if(vue.status.current_stage == "aitutu_done"){
199 | subs.push(h("Button",{
200 | props: {
201 | type:vue.redoArr.includes(`p_${params.row.seqnum}:${tutu.id}`)?"error":"info",
202 | icon:"md-refresh",
203 | shape:"circle",
204 | loading:vue.redoArr.includes(`p_${params.row.seqnum}:${tutu.id}`),
205 | },
206 | on:{
207 | click:()=>{
208 | vue.doRegenTutu(this,params.row,tutu)
209 | }
210 | },
211 | }));
212 | }
213 | let tutuDiv = h("div",{
214 | class:"tutu-div",
215 | },subs);
216 | tutuArr.push(tutuDiv);
217 | }
218 | return h("div",{
219 | class:"tutu-column-div",
220 | },tutuArr);
221 | }
222 | },],
223 | }
224 | },
225 | computed:{
226 | cal_step_status(){
227 | let ss = 0;
228 | switch(this.status.current_stage){
229 | case "split_start":
230 | case "split_ing":
231 | case "split_done":
232 | case "tts_start":
233 | case "tts_ing":
234 | case "tts_done":
235 | ss = 1;
236 | break;
237 | case "aiprompt_start":
238 | case "aiprompt_ing":
239 | case "aiprompt_done":
240 | ss = 2;
241 | break;
242 | case "aitutu_start":
243 | case "aitutu_ing":
244 | case "aitutu_done":
245 | ss = 3;
246 | break;
247 | case "video_start":
248 | case "video_ing":
249 | case "video_done":
250 | ss = 4;
251 | break;
252 | case "merge_video_start":
253 | case "merge_video_ing":
254 | case "merge_video_done":
255 | ss = 5;
256 | break;
257 | default:
258 | ss = 0;
259 | break;
260 | }
261 | return ss;
262 | },
263 | },
264 | methods:{
265 | initApp(){
266 | let vue = this;
267 | Utils.ipcSend('get-config');
268 | Utils.ipcReceive('get-config-done', (data) => {
269 | this.status.work_dir = data.config.work_dir;
270 | if(!!!data.config.work_dir){
271 | vue.$Modal.info({
272 | title: "警告",
273 | content: "请先设置工作目录",
274 | onOk:()=>{
275 | Utils.ipcSend("to-open-setting-page");
276 | }
277 | });
278 | return;
279 | }
280 | vue.options = data.options;
281 | });
282 | },
283 | changeActionType(){
284 |
285 | if(!!!this.status.work_dir){
286 | this.$Modal.info({
287 | title: "警告",
288 | content: "请先设置工作目录",
289 | onOk:()=>{
290 | Utils.ipcSend("to-open-setting-page");
291 | }
292 | });
293 | return;
294 | }
295 | this.status.newFilePath = "";
296 | this.status.temp_dir = "";
297 | this.status.role_name = "";
298 | this.status.rate = "";
299 | this.status.volume = "";
300 |
301 | if(this.status.actionType == "new"){
302 | Utils.copyValues(this.defaultStatus,this.status);
303 | }
304 | if(this.status.actionType == "continue"){
305 | Utils.ipcSend("get-temp-dirs");
306 | Utils.ipcReceive('get-temp-dirs-done', (data) => {
307 | this.continueList = data.dirs;
308 | });
309 | }
310 | },
311 | toSelectNewFile(){
312 | this.$refs['ltext-file'].click();
313 | },
314 | selectNewFile(e){
315 | if(this.$refs['ltext-file'].files.length > 0){
316 | this.status.newFilePath = this.$refs['ltext-file'].files[0].path;
317 | }
318 | },
319 | selectContinueDir(){
320 | Utils.ipcSend("get-continue-status",this.status.continueDir);
321 | Utils.ipcReceive('get-continue-status-done', (data) => {
322 | Utils.copyValues(data.status, this.status);
323 | });
324 | },
325 | refreshCurrentStatus(){
326 | if(!!!this.status.temp_dir){
327 | return;
328 | }
329 | let vue = this;
330 | Utils.ipcSend("refresh-status",vue.status.temp_dir);
331 | Utils.ipcReceive('refresh-status-done', (data) => {
332 | console.log("refreshCurrentStatus");
333 | console.log(data);
334 | if(data.rslt){
335 | Utils.copyValues(data.status, vue.status);
336 | if(!!data.status.sces){
337 | vue.sces = data.status.sces;
338 | }
339 | }
340 | });
341 |
342 | },
343 | autoRefreshCurrentStatus(){
344 | let vue = this;
345 | vue.refreshCurrentStatus();
346 | if(!!vue.refreshTimer){
347 | clearTimeout(vue.refreshTimer);
348 | }
349 | vue.refreshTimer = setTimeout(() => {
350 | vue.autoRefreshCurrentStatus();
351 | }, 2000);
352 | },
353 | async next0(){
354 |
355 | if(!!!this.status.work_dir){
356 | this.$Modal.info({
357 | title: "警告",
358 | content: "请先设置工作目录",
359 | onOk:()=>{
360 | Utils.ipcSend("to-open-setting-page");
361 | }
362 | });
363 | return;
364 | }
365 | if(this.status.actionType == "new"){
366 | if(!!!this.status.newFilePath){
367 | Message.error("请选择文件。");
368 | return;
369 | }
370 | }else{
371 | if(!!!this.status.continueDir){
372 | Message.error("请选择要继续处理的目录。");
373 | return;
374 | }
375 | }
376 | if(!!!this.status.role_name){
377 | Message.error("请选择解说角色。");
378 | return;
379 | }
380 | if(!!!this.status.rate){
381 | Message.error("请选择语速。");
382 | return;
383 | }
384 | if(!!!this.status.volume){
385 | Message.error("请选择音量。");
386 | return;
387 | }
388 | if(this.status.actionType == "new"){
389 | this.loading.next0 = true;
390 | this.status.temp_dir = Utils.generateTimestampedRandomString();
391 | Utils.ipcSend("run-split",this.status);
392 | Utils.ipcReceive('split-done', (data) => {
393 | if(data.code !=0){
394 | Message.error("执行失败,请查看日志");
395 | return;
396 | }
397 | this.step = 1;
398 | this.loading.next0 = false;
399 | });
400 | }else{
401 | let vue = this;
402 | vue.loading.next0 = true;
403 | vue.refreshCurrentStatus();
404 | vue.loading.next0 = false;
405 | vue.step = vue.cal_step_status;
406 | }
407 | },
408 | async next1_start(){
409 | this.loading.next1 = true;
410 | Utils.ipcSend("run-batch-tts",this.status);
411 | this.autoRefreshCurrentStatus();
412 | let vue = this;
413 | Utils.ipcReceive('run-batch-tts-done', (data) => {
414 | console.log('run-batch-tts-done');
415 | console.log(data);
416 | if(data.code !=0){
417 | Message.error("执行失败,请查看日志");
418 | return;
419 | }
420 | if(!!vue.refreshTimer){
421 | clearTimeout(vue.refreshTimer);
422 | }
423 | Utils.copyValues(data.rslt.status, vue.status);
424 | if(!!data.rslt.status.sces){
425 | vue.sces = data.rslt.status.sces;
426 | }
427 | vue.loading.next1 = false;
428 | });
429 | },
430 | async next1_stop(){
431 | this.step = 2;
432 | console.log(this.status)
433 | console.log(this.sces)
434 | },
435 | async next2_start(){
436 | this.loading.next2 = true;
437 | Utils.ipcSend("run-ai-prompt",this.status);
438 | this.autoRefreshCurrentStatus();
439 | let vue = this;
440 | Utils.ipcReceive('run-ai-prompt-done', (data) => {
441 | console.log('run-ai-prompt-done');
442 | console.log(data);
443 | if(data.code !=0){
444 | Message.error("执行失败,请查看日志");
445 | return;
446 | }
447 | if(!!vue.refreshTimer){
448 | clearTimeout(vue.refreshTimer);
449 | }
450 | Utils.copyValues(data.rslt.status, vue.status);
451 | if(!!data.rslt.status.sces){
452 | vue.sces = data.rslt.status.sces;
453 | }
454 | vue.loading.next2 = false;
455 | });
456 | },
457 | async next2_stop(){
458 | this.step = 3;
459 | console.log(this.status)
460 | console.log(this.sces)
461 | },
462 | async openTextModal(row){
463 | this.$Modal.info({
464 | content: row.text
465 | });
466 | },
467 | async openPromptModal(row){
468 | this.promptModal.row = row;
469 | this.promptModal.show = true;
470 | this.promptModal.readonly = false;
471 | },
472 | async prompt_save(){
473 | let sces = this.sces;
474 | for(let sce of sces){
475 | if(sce.seqnum == this.promptModal.row.seqnum){
476 | Utils.copyValues(this.promptModal.row, sce);
477 | }
478 | }
479 | this.loading.prompt_save = true;
480 | Utils.ipcSend("run-prompt-save",this.status);
481 | let vue = this;
482 | Utils.ipcReceive('run-prompt-save-done', (data) => {
483 | if(data.code !=0){
484 | Message.error("执行失败,请查看日志");
485 | vue.loading.prompt_save = false;
486 | return;
487 | }
488 | if(!!vue.refreshTimer){
489 | clearTimeout(vue.refreshTimer);
490 | }
491 | Utils.copyValues(data.rslt.status, vue.status);
492 | if(!!data.rslt.status.sces){
493 | vue.sces = data.rslt.status.sces;
494 | }
495 | vue.loading.prompt_save = false;
496 | vue.promptModal.show = false;
497 | });
498 | },
499 | async prompt_regen(){
500 | this.loading.prompt_regen = true;
501 | Utils.ipcSend("run-regen-prompt",{
502 | status:this.status,
503 | sce:this.promptModal.row,
504 | });
505 | let vue = this;
506 | Utils.ipcReceive('run-regen-prompt-done', (data) => {
507 | if(data.code !=0){
508 | Message.error("执行失败,请查看日志");
509 | vue.loading.prompt_regen = false;
510 | return;
511 | }
512 | if(!!vue.refreshTimer){
513 | clearTimeout(vue.refreshTimer);
514 | }
515 | Utils.copyValues(data.rslt.status, vue.status);
516 | if(!!data.rslt.status.sces){
517 | vue.sces = data.rslt.status.sces;
518 | }
519 | vue.loading.prompt_regen = false;
520 | vue.promptModal.show = false;
521 | });
522 | },
523 | async regenPromptTask(data){
524 | console.log("开始执行这次任务");
525 | Utils.ipcSend("run-regen-prompt",data);
526 |
527 | let vue = this;
528 |
529 | Utils.ipcReceive('run-regen-prompt-done', (data) => {
530 | console.log("queue done run-regen-prompt-done");
531 | console.log(data);
532 | if(data.code !=0){
533 | Message.error("执行失败,请查看日志");
534 | }else{
535 | if(!!vue.refreshTimer){
536 | clearTimeout(vue.refreshTimer);
537 | }
538 | Utils.copyValues(data.rslt.status, vue.status);
539 | if(!!data.rslt.status.sces){
540 | vue.sces = data.rslt.status.sces;
541 | for(let i = vue.redoArr.length - 1 ; i>=0;i--){
542 | if(vue.redoArr[i] == `p_${data.seqnum}`){
543 | vue.redoArr.splice(i,1);
544 | }
545 | }
546 | }
547 | }
548 | console.log("调用doneProcessing")
549 | vue.taskQueue.doneProcessing();
550 | });
551 | },
552 | async doRegenPrompt(e,row){
553 | this.redoArr.push(`p_${row.seqnum}`);
554 | this.taskQueue.addQueue(this.regenPromptTask,{
555 | status:this.status,
556 | sce:row,
557 | });
558 | },
559 | async prompt_return(){
560 | this.promptModal.show = false;
561 | this.promptModal.row = {};
562 | },
563 | resetAitableSize(){
564 | this.aitableHeight = window.innerHeight - 290;
565 | console.log(this.aitableHeight);
566 | },
567 |
568 | async next3_start(){
569 | this.loading.next3 = true;
570 | Utils.ipcSend("run-ai-tutu",this.status);
571 | this.autoRefreshCurrentStatus();
572 | let vue = this;
573 | Utils.ipcReceive('run-ai-tutu-done', (data) => {
574 | console.log('run-ai-tutu-done');
575 | console.log(data);
576 | if(data.code !=0){
577 | Message.error("执行失败,请查看日志");
578 | return;
579 | }
580 | if(!!vue.refreshTimer){
581 | clearTimeout(vue.refreshTimer);
582 | }
583 | Utils.copyValues(data.rslt.status, vue.status);
584 | if(!!data.rslt.status.sces){
585 | vue.sces = data.rslt.status.sces;
586 | }
587 | vue.loading.next3 = false;
588 | });
589 | },
590 | async next3_stop(){
591 | this.step = 4;
592 | console.log(this.status)
593 | console.log(this.sces)
594 | },
595 | async tutu_regen(){
596 | this.loading.tutu_regen = true;
597 | let tutuid = `${this.tutuModal.row.seqnum}:${this.tutuModal.tutu.id}`;
598 | Utils.ipcSend("run-regen-tutu",{
599 | status:this.status,
600 | tutuid,
601 | });
602 | let vue = this;
603 | Utils.ipcReceive('run-regen-tutu-done', (data) => {
604 | if(data.code !=0){
605 | Message.error("执行失败,请查看日志");
606 | vue.loading.tutu_regen = false;
607 | return;
608 | }
609 | if(!!vue.refreshTimer){
610 | clearTimeout(vue.refreshTimer);
611 | }
612 | Utils.copyValues(data.rslt.status, vue.status);
613 | if(!!data.rslt.status.sces){
614 | vue.sces = data.rslt.status.sces;
615 | }
616 | vue.loading.tutu_regen = false;
617 | vue.tutuModal.show = false;
618 | });
619 | },
620 | async regenTutuTask(data){
621 | console.log("开始执行这次任务");
622 | Utils.ipcSend("run-regen-tutu",data);
623 |
624 | let vue = this;
625 |
626 | Utils.ipcReceive('run-regen-tutu-done', (data) => {
627 | console.log("queue done run-regen-tutu-done");
628 | console.log(data);
629 | if(data.code !=0){
630 | Message.error("执行失败,请查看日志");
631 | }else{
632 | if(!!vue.refreshTimer){
633 | clearTimeout(vue.refreshTimer);
634 | }
635 | Utils.copyValues(data.rslt.status, vue.status);
636 | if(!!data.rslt.status.sces){
637 | vue.sces = data.rslt.status.sces;
638 | for(let i = vue.redoArr.length - 1 ; i>=0;i--){
639 | if(vue.redoArr[i] == `p_${data.tutuid}`){
640 | vue.redoArr.splice(i,1);
641 | }
642 | }
643 | }
644 | }
645 | console.log("调用doneProcessing")
646 | vue.taskQueue.doneProcessing();
647 | });
648 | },
649 | async doRegenTutu(e,row,tutu){
650 | let tutuid = `${row.seqnum}:${tutu.id}`;
651 | this.redoArr.push(`p_${tutuid}`);
652 | this.taskQueue.addQueue(this.regenTutuTask,{
653 | status:this.status,
654 | tutuid,
655 | });
656 | },
657 | async openTutuTextModal(row){
658 | this.promptModal.row = row;
659 | this.promptModal.show = true;
660 | this.promptModal.readonly = true;
661 | },
662 | async tutuModalShow(e,row,tutu){
663 | this.tutuModal.show = true;
664 | this.tutuModal.row = row;
665 | this.tutuModal.tutu = tutu;
666 | },
667 | async tutuModalReturn(){
668 | this.tutuModal.show = false;
669 | this.tutuModal.row = {};
670 | this.tutuModal.tutu = {};
671 | },
672 | async tutu_next(){
673 | let crtRow = this.tutuModal.row;
674 | let crtTutu = this.tutuModal.tutu;
675 | let sces = this.sces;
676 |
677 | if(crtTutu.id + 1 >= crtRow.tutu.length){
678 | //已经是本场景最后一张图片,取下一个场景第一张图片
679 | if(crtRow.seqnum >= sces.length){
680 | //已经是最后一个场景了
681 | Message.error("没有下一个了。");
682 | return;
683 |
684 | }else{
685 | let nextSce = sces[crtRow.seqnum]
686 | let tutu = nextSce.tutu[0];
687 | this.tutuModal.row = nextSce;
688 | this.tutuModal.tutu = tutu;
689 | }
690 | }else{
691 | //取本场景下一张图片
692 | let nextSce = crtRow;
693 | let tutu = nextSce.tutu[crtTutu.id+1];
694 | this.tutuModal.row = nextSce;
695 | this.tutuModal.tutu = tutu;
696 | }
697 | },
698 | async tutu_last(){
699 | let crtRow = this.tutuModal.row;
700 | let crtTutu = this.tutuModal.tutu;
701 | let sces = this.sces;
702 |
703 | if(crtTutu.id == 0){
704 | //已经是本场景第一张图片,取上一个场景最后一张图片
705 | if(crtRow.seqnum == 1){
706 | //已经是第一个场景了
707 | Message.error("没有上一个了。");
708 | return;
709 |
710 | }else{
711 | let lastSce = sces[crtRow.seqnum-2]
712 | let tutu = lastSce.tutu[lastSce.tutu.length-1];
713 | this.tutuModal.row = lastSce;
714 | this.tutuModal.tutu = tutu;
715 | }
716 | }else{
717 | //取本场景上一张图片
718 | let lastSce = crtRow;
719 | let tutu = lastSce.tutu[crtTutu.id-1];
720 | this.tutuModal.row = lastSce;
721 | this.tutuModal.tutu = tutu;
722 | }
723 |
724 | },
725 | async next4_start(){
726 | this.loading.next4 = true;
727 | Utils.ipcSend("run-gen-video",this.status);
728 | this.autoRefreshCurrentStatus();
729 | let vue = this;
730 | Utils.ipcReceive('run-gen-video-done', (data) => {
731 | console.log('run-gen-video-done');
732 | console.log(data);
733 | if(data.code !=0){
734 | Message.error("执行失败,请查看日志");
735 | return;
736 | }
737 | if(!!vue.refreshTimer){
738 | clearTimeout(vue.refreshTimer);
739 | }
740 | Utils.copyValues(data.rslt.status, vue.status);
741 | if(!!data.rslt.status.sces){
742 | vue.sces = data.rslt.status.sces;
743 | }
744 | vue.loading.next4 = false;
745 | });
746 | },
747 | async next4_stop(){
748 | this.step = 5;
749 | console.log(this.status)
750 | console.log(this.sces)
751 | },
752 | async showVideo(row){
753 | if(!!row.mp4aufile){
754 | window.open(row.mp4aufile);
755 | }
756 | },
757 | async next5_start(){
758 | this.loading.next5 = true;
759 | Utils.ipcSend("run-merge-mp4",this.status);
760 | this.autoRefreshCurrentStatus();
761 | let vue = this;
762 | Utils.ipcReceive('run-merge-mp4-done', (data) => {
763 | console.log('run-merge-mp4-done');
764 | console.log(data);
765 | if(data.code !=0){
766 | Message.error("执行失败,请查看日志");
767 | return;
768 | }
769 | if(!!vue.refreshTimer){
770 | clearTimeout(vue.refreshTimer);
771 | }
772 | Utils.copyValues(data.rslt.status, vue.status);
773 | if(!!data.rslt.status.sces){
774 | vue.sces = data.rslt.status.sces;
775 | }
776 | vue.loading.next5 = false;
777 | });
778 | },
779 | async next5_stop(){
780 | Utils.ipcSend('open-directory', `${this.status.work_dir}\\cache\\${this.status.temp_dir}`);
781 | },
782 | async playVideo(){
783 | let url = `${this.status.work_dir}/cache/${this.status.temp_dir}/${this.status.source_file_name}.mp4`
784 | console.log(url)
785 | window.open(url)
786 | },
787 | },
788 | created(){
789 | this.initApp();
790 | },
791 | mounted(){
792 | this.taskQueue = initQueue();
793 | this.resetAitableSize();
794 | window.addEventListener('resize', this.resetAitableSize);
795 | },
796 | beforeDestroy() {
797 | window.removeEventListener('resize', this.resetAitableSize);
798 | },
799 | }
800 |
--------------------------------------------------------------------------------