├── 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 | 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 | ![x](1.png) 26 | ![x](2.png) 27 | 28 | ![x](3.png) 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------