数字人直播问题修改

main
xiangcongshuai 1 month ago
parent 76347132b4
commit ccef719c06

@ -3,13 +3,13 @@ const path = require("path");
const { app } = require("electron");
const { spawn, exec } = require("child_process");
// 进程引用管理
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ù<EFBFBD><EFBFBD><EFBFBD>
const processes = {
LiveTalkingProcess: null,
gptsovitsProcess: null,
chatProcess: null
};
// 获取数据库路径
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD>·<EFBFBD><EFBFBD>
function getDatabasePath() {
if (process.env.NODE_ENV === "development") {
const dbPath = path.join(__dirname, "..", "..", "live_chat.db");
@ -17,28 +17,28 @@ function getDatabasePath() {
} else {
const exePath = app.getPath("exe");
// 获取win-unpacked文件夹路径
// <EFBFBD><EFBFBD>ȡwin-unpacked<65>ļ<EFBFBD><C4BC><EFBFBD>·<EFBFBD><C2B7>
const winUnpackedDir = path.dirname(exePath);
// 获取win-unpacked的上级目录与win-unpacked同级的目录
// <EFBFBD><EFBFBD>ȡwin-unpacked<65><64><EFBFBD>ϼ<EFBFBD>Ŀ¼<C4BF><C2BC><EFBFBD><EFBFBD>win-unpackedͬ<64><CDAC><EFBFBD><EFBFBD>Ŀ¼<C4BF><C2BC>
const parentDir = path.dirname(winUnpackedDir);
// 构建live_chat.db的完整路径
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>live_chat.db<64><62><EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><C2B7>
const dbPath = path.join(parentDir, "live_chat.db");
console.log("dbPath", dbPath);
return dbPath;
}
}
// 更新配置值
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ
function updateConfig(tableName, key, value) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
if (err) {
console.error("数据库连接错误:", err.message);
console.error("<EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӵ<EFBFBD><EFBFBD><EFBFBD>:", err.message);
return reject(err);
}
});
// 先尝试更新如果影响行数为0则插入新记录
// <EFBFBD>ȳ<EFBFBD><EFBFBD>Ը<EFBFBD><EFBFBD>£<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ0<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>¼
db.run(
`UPDATE ${tableName} SET value = ? WHERE key = ?`,
[value, key],
@ -48,7 +48,7 @@ function updateConfig(tableName, key, value) {
return reject(err);
}
// 如果没有匹配的记录则插入
// <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>ƥ<EFBFBD><EFBFBD>ļ<EFBFBD>¼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (this.changes === 0) {
db.run(
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)`,
@ -72,7 +72,7 @@ function updateConfig(tableName, key, value) {
});
}
// 获取配置值
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ
function getConfigValue(tableName, key) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
@ -92,12 +92,44 @@ function getConfigValue(tableName, key) {
);
});
}
// 批量插入系统消息
function getAllSystemMessages() {
return new Promise((resolve, reject) => {
// 连接数据库
const db = new sqlite3.Database(getDatabasePath(), err => {
if (err) {
console.error("数据库连接错误:", err.message);
return reject(err);
}
console.log("成功连接到数据库");
});
// 执行查询
const sql = "SELECT message FROM system_message";
db.all(sql, [], (err, rows) => {
// 关闭数据库连接
db.close(closeErr => {
if (closeErr) {
console.error("关闭数据库错误:", closeErr.message);
return reject(closeErr);
}
});
if (err) {
console.error("查询错误:", err.message);
return reject(err);
}
// 提取所有message字段
const messages = rows.map(row => row.message);
resolve(messages);
});
});
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵͳ<CFB5><CDB3>Ϣ
const bulkInsertSystemMessages = messages => {
console.log("系统消息:", messages);
return new Promise((resolve, reject) => {
if (!Array.isArray(messages) || messages.length === 0) {
reject(new Error("请提供有效的消息数组"));
reject(new Error("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>"));
return;
}
@ -108,7 +140,7 @@ const bulkInsertSystemMessages = messages => {
return;
}
// 使用事务进行批量插入,提高性能
// ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
db.run("BEGIN TRANSACTION", err => {
if (err) {
reject(err);
@ -130,7 +162,7 @@ const bulkInsertSystemMessages = messages => {
completed++;
// 所有消息都处理完毕
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (completed === messages.length) {
stmt.finalize();
@ -162,7 +194,7 @@ const bulkInsertSystemMessages = messages => {
}
});
};
// 清空系统消息表
// <EFBFBD><EFBFBD><EFBFBD>ϵͳ<EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD>
const clearSystemMessages = () => {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
@ -171,7 +203,7 @@ const clearSystemMessages = () => {
return;
}
// 使用TRUNCATE-like操作清空表同时重置自增ID
// ʹ<EFBFBD><EFBFBD>TRUNCATE-like<6B><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ձ<EFBFBD><D5B1><EFBFBD>ͬʱ<CDAC><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ID
db.run("DELETE FROM system_message", err => {
if (err) {
reject(err);
@ -179,7 +211,7 @@ const clearSystemMessages = () => {
return;
}
// 重置自增计数器SQLite特定
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>SQLite<EFBFBD>ض<EFBFBD><EFBFBD><EFBFBD>
db.run(
"DELETE FROM sqlite_sequence WHERE name = 'system_message'",
err => {
@ -198,31 +230,31 @@ const clearSystemMessages = () => {
function getProcessExePath(fileName, exeName) {
let exePath;
let exeFolder;
// 判断是否为开发环境
// <EFBFBD>ж<EFBFBD><EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (process.env.NODE_ENV === "development") {
// process.cwd() 通常指向项目根目录
// process.cwd() ͨ<EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><EFBFBD>Ŀ¼
const projectRoot = process.cwd();
const parentDir = path.dirname(projectRoot);
exePath = path.join(parentDir, fileName, exeName);
exeFolder = path.join(parentDir, fileName);
} else {
// 生产环境: 与win-unpacked同级目录下的LiveTalking文件夹
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><>win-unpackedͬ<64><CDAC>Ŀ¼<C4BF>µ<EFBFBD>LiveTalking<6E>ļ<EFBFBD><C4BC><EFBFBD>
exePath = app.getPath("exe");
// 获取win-unpacked文件夹路径
// <EFBFBD><EFBFBD>ȡwin-unpacked<65>ļ<EFBFBD><C4BC><EFBFBD>·<EFBFBD><C2B7>
const winUnpackedDir = path.dirname(exePath);
// 获取win-unpacked的上级目录
// <EFBFBD><EFBFBD>ȡwin-unpacked<65><64><EFBFBD>ϼ<EFBFBD>Ŀ¼
const parentDir = path.dirname(winUnpackedDir);
exePath = path.join(parentDir, fileName, exeName);
exeFolder = path.join(parentDir, fileName);
}
console.log("exePath", exePath);
console.log("exeFolder", exeFolder);
// 标准化路径格式
// <EFBFBD><EFBFBD>׼<EFBFBD><EFBFBD>·<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ
return { exePath, exeFolder };
}
/**
* 启动LiveTalking.exe
* <EFBFBD><EFBFBD><EFBFBD><EFBFBD>LiveTalking.exe
*/
async function startProcess(fileName, exeName) {
return new Promise(async (resolve, reject) => {
@ -232,38 +264,48 @@ async function startProcess(fileName, exeName) {
return;
}
// 获取程序路径
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><EFBFBD>
const obj = getProcessExePath(fileName, exeName);
try {
// 启动进程时通过wmic获取详细信息仅Windows
const newProcess = spawn("cmd.exe", ["/c", obj.exePath], {
// 直接运行bat不使用start命令
cwd: obj.exeFolder,
windowsVerbatimArguments: true,
windowsHide: false,
detached: false // 不分离,便于跟踪子进程
});
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD>wmic<EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ϸ<EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Windows<EFBFBD><EFBFBD>
// const newProcess = spawn("cmd.exe", ["/c", obj.exePath], {
// // ֱ<><D6B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>bat<61><74><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9>start<72><74><EFBFBD><EFBFBD>
// cwd: obj.exeFolder,
// windowsVerbatimArguments: true,
// windowsHide: false,
// detached: false // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EBA3AC><EFBFBD>ڸ<EFBFBD><DAB8><EFBFBD><EFBFBD>ӽ<EFBFBD><D3BD><EFBFBD>
// });
// 记录进程ID关键保存实际PID
const newProcess = spawn(
"cmd.exe",
["/c", "start", '""', `"${obj.exePath}"`],
{
cwd: obj.exeFolder,
windowsVerbatimArguments: true,
windowsHide: false,
detached: true
}
);
// <20><>¼<EFBFBD><C2BC><EFBFBD><EFBFBD>ID<49><44><EFBFBD>ؼ<EFBFBD><D8BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5>PID<49><44>
processes[processKey] = {
process: newProcess,
pid: newProcess.pid // 保存PID用于后续终止
pid: newProcess.pid // <EFBFBD><EFBFBD><EFBFBD><EFBFBD>PID<EFBFBD><EFBFBD><EFBFBD>ں<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֹ
};
newProcess.on("error", err => {
processes[processKey] = null;
reject(`启动失败: ${err.message}`);
reject(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>: ${err.message}`);
});
console.log(`spawned with PID ${newProcess.pid}`);
resolve(`${fileName} 启动成功 (PID: ${newProcess.pid})`);
resolve(`${fileName} <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD> (PID: ${newProcess.pid})`);
} catch (err) {
reject(`启动异常: ${err.message}`);
reject(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: ${err.message}`);
}
});
}
// 停止所有进程
// ֹͣ<EFBFBD><EFBFBD><EFBFBD>н<EFBFBD><EFBFBD><EFBFBD>
async function stopAllProcesses() {
try {
// 清空所有属性,变成空对象
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD><EFBFBD><EFBFBD>ɿն<EFBFBD><EFBFBD><EFBFBD>
for (const key in processes) {
if (processes[key]) {
delete processes[key];
@ -289,5 +331,6 @@ module.exports = {
bulkInsertSystemMessages,
clearSystemMessages,
startProcess,
stopAllProcesses
stopAllProcesses,
getAllSystemMessages
};

@ -7,7 +7,9 @@ const {
bulkInsertSystemMessages,
stopProcess,
startProcess,
stopAllProcesses
stopAllProcesses,
getAllSystemMessages,
clearSystemMessages
} = require("./dbHandler");
// 注册IPC通信处理
@ -38,6 +40,15 @@ function registerIpcHandlers() {
throw error;
}
});
ipcMain.handle("clear-system-messages", async () => {
try {
return await clearSystemMessages();
} catch (error) {
console.error("停止失败:", error);
throw error;
}
});
ipcMain.handle("start-process", async (event, fileName, exeName) => {
try {
return await startProcess(fileName, exeName);
@ -62,6 +73,14 @@ function registerIpcHandlers() {
throw error;
}
});
ipcMain.handle("get-all-system-messages", async () => {
try {
return await getAllSystemMessages();
} catch (error) {
console.error("停止失败:", error);
throw error;
}
});
}
function createWindow() {
const win = new BrowserWindow({

@ -16,5 +16,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
ipcRenderer.invoke("start-process", fileName, exeName),
stopProcess: (fileName, exeName) =>
ipcRenderer.invoke("stop-process", fileName, exeName),
stopAllProcesses: () => ipcRenderer.invoke("stop-all-process")
stopAllProcesses: () => ipcRenderer.invoke("stop-all-process"),
clearSystemMessages: () => ipcRenderer.invoke("clear-system-messages"),
getAllSystemMessages: () => ipcRenderer.invoke("get-all-system-messages")
});

@ -7,7 +7,7 @@
<meta name="renderer" content="webkit" />
<meta name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" />
<title>智能小助手</title>
<title>直播数字人</title>
<link rel="icon" href="/favicon.ico" />
<script>
window.process = {};

24534
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -110,7 +110,6 @@
"vue": "^3.3.4",
"vue-router": "^4.2.2",
"vue-types": "^5.1.0",
"vue-webrtc": "^3.0.1",
"vue3-seamless-scroll": "^2.0.1",
"wangeditor": "^4.7.15"
},

File diff suppressed because it is too large Load Diff

@ -3,7 +3,7 @@ import config from "@/utils/httpProxy";
export const getSalespitch = (data?: object) => {
return http.request<any>(
"post",
`${config.services.liveDigital}/generate/salespitch`,
`${config.services.liveDigital}/liveDigital/generate/salespitch`,
{ data }
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@ -8,7 +8,7 @@ import router from "@/router";
const { logout, userAvatar, avatarsStyle } = useNav();
import { useRoute } from "vue-router";
const route = useRoute();
const userName = ref("sdsd");
const userName = ref("admin");
onMounted(() => {});
</script>
@ -16,7 +16,7 @@ onMounted(() => {});
<div class="NavBar">
<div class="nav-left">
<img :src="videoImg" alt="" />
<span>数字人直播</span>
<span>直播数字人</span>
</div>
<!-- 退出登录 -->
<el-dropdown trigger="click" class="nav-right">

@ -7,5 +7,7 @@ interface Window {
startProcess: (fileName, exeName) => void;
stopProcess: (fileName, exeName) => void;
stopAllProcesses: () => void;
getAllSystemMessages: () => Promise<any>;
clearSystemMessages: () => Promise<void>;
};
}

@ -5,6 +5,6 @@ export default {
services: {
liveDigital: isDevelopment
? "/live-digital-avatar-manage"
: "http://192.168.10.25:9909/live-digital-avatar-manage"
: "http://192.168.10.137:9909/live-digital-avatar-manage"
}
};

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import { reactive, ref } from "vue";
import { reactive, ref, onMounted } from "vue";
import cheackImg from "@/assets/svg/live/check.svg";
import starImg from "@/assets/svg/live/star.svg";
import plusImg from "@/assets/svg/live/plus.svg";
@ -25,9 +25,38 @@ const formData = reactive({
}
]
});
//
const validateSpecifications = (rule, value, callback) => {
//
if (value.length === 0) {
return callback(new Error("请至少添加一个商品规格"));
}
//
for (let i = 0; i < value.length; i++) {
const item = value[i];
if (!item.size.trim()) {
return callback(new Error(`${i + 1} 个规格的规格名称不能为空`));
}
if (!item.price.trim()) {
return callback(new Error(`${i + 1} 个规格的价格不能为空`));
}
//
if (isNaN(Number(item.price)) || Number(item.price) <= 0) {
return callback(new Error(`${i + 1} 个规格的价格格式不正确`));
}
}
//
callback();
};
const loading = ref(false);
const rules = {
script: [{ required: true, message: "请输入", trigger: "change" }]
productName: [
{ required: true, message: "请输入商品名称", trigger: "change" }
],
specifications: [{ required: true, validator: validateSpecifications }],
detail: [{ required: true, message: "请输入商品详情", trigger: "change" }]
};
const handlePriceInput = item => {
@ -44,6 +73,31 @@ const addNorms = () => {
price: ""
});
};
const save = () => {
if (!ruleFormRef.value) return;
ruleFormRef.value.validate(valid => {
if (valid) {
window.electronAPI.updateConfig(
"live_config",
"product_name",
formData.productName
);
window.electronAPI.updateConfig(
"live_config",
"product_specification",
JSON.stringify(formData.specifications)
);
window.electronAPI.updateConfig(
"live_config",
"product_description",
formData.detail
);
message("商品信息已保存", { type: "success" });
} else {
message("请填写完整信息", { type: "warning" });
}
});
};
const generateScript = async () => {
loading.value = true;
buttonText.value = "正在生成";
@ -56,13 +110,44 @@ const generateScript = async () => {
buttonText.value = "生成AI话术";
if (res.code === 200) {
salespitchList.value = res.data;
// await window.electronAPI.clearSystemMessages();
const result = await window.electronAPI.bulkInsertSystemMessages(res.data);
if (result.success) {
message("生成成功!", { type: "success", showClose: true });
}
message("生成成功!", { type: "success", showClose: true });
await window.electronAPI.clearSystemMessages();
window.electronAPI.bulkInsertSystemMessages(res.data);
}
};
onMounted(async () => {
//
const productName = await window.electronAPI.getConfig(
"live_config",
"product_name"
);
const productSpecification = await window.electronAPI.getConfig(
"live_config",
"product_specification"
);
const productDescription = await window.electronAPI.getConfig(
"live_config",
"product_description"
);
if (productName) {
formData.productName = productName;
}
if (productSpecification) {
try {
formData.specifications = JSON.parse(productSpecification);
} catch (error) {
console.error("解析商品规格失败:", error);
}
}
if (productDescription) {
formData.detail = productDescription;
}
// AI
const res = await window.electronAPI.getAllSystemMessages();
if (res && Array.isArray(res)) {
salespitchList.value = res;
}
});
</script>
<template>
@ -100,7 +185,12 @@ const generateScript = async () => {
label-position="top"
>
<el-form-item label="商品名称" prop="productName">
<el-input size="large" v-model="formData.productName" />
<el-input
size="large"
maxlength="100"
show-word-limit
v-model="formData.productName"
/>
</el-form-item>
<el-form-item label="商品规格" prop="specifications">
@ -113,12 +203,16 @@ const generateScript = async () => {
class="norms-item"
size="large"
v-model="item.size"
maxlength="20"
show-word-limit
placeholder="请输入商品规格"
/>
<el-input
class="norms-item"
size="large"
v-model="item.price"
maxlength="20"
show-word-limit
placeholder="请输入商品价格"
@input="handlePriceInput(item)"
/>
@ -143,6 +237,8 @@ const generateScript = async () => {
<el-input
:rows="8"
type="textarea"
maxlength="500"
show-word-limit
placeholder="请输入"
v-model="formData.detail"
/>
@ -155,12 +251,12 @@ const generateScript = async () => {
v-model="formData.script"
/>
</el-form-item> -->
<div class="tips">
<!-- <div class="tips">
<tipsImg />
<span
>AI商品讲解话术由大模型基于上述内容自动生成还能结合直播时长和商品特点生成适配的A!数字人口播内容</span
>
</div>
</div> -->
<div class="footer-btns">
<el-button class="reset" @click="ruleFormRef.resetFields()"
>重置</el-button
@ -168,6 +264,7 @@ const generateScript = async () => {
<el-button :loading="loading" type="primary" @click="generateScript">
{{ buttonText }}</el-button
>
<el-button type="primary" @click="save"> </el-button>
</div>
</el-form>
</div>

@ -5,6 +5,7 @@ import editImg from "@/assets/live/edit.png";
import GoodsManage from "./components/GoodsManage.vue";
import InteractionScript from "./components/InteractionScript.vue";
import BasicSettings from "./components/BasicSettings.vue";
import { message } from "@/utils/message";
const router = useRouter();
const liveName = ref("慧文的直播间");
const activedKey = ref("1");
@ -19,12 +20,41 @@ const BasicSettingsRef = ref(undefined);
const chanageActived = (key: string) => {
activedKey.value = key;
};
const validateProductInfo = async () => {
//
const productName = await window.electronAPI.getConfig(
"live_config",
"product_name"
);
const productSpecification = await window.electronAPI.getConfig(
"live_config",
"product_specification"
);
const productDescription = await window.electronAPI.getConfig(
"live_config",
"product_description"
);
//
const emptyFields = [];
if (!productName?.trim()) emptyFields.push("商品名称");
if (!productSpecification?.trim()) emptyFields.push("商品规格");
if (!productDescription?.trim()) emptyFields.push("商品描述");
//
if (emptyFields.length > 0) {
message(`以下字段不能为空: ${emptyFields.join("、")}`, { type: "error" });
return false;
} else {
return true;
}
};
const enterLiveRoom = async () => {
const isValid = await validateProductInfo();
if (!isValid) return;
router.push({
path: "/Live",
query: { gender: router.currentRoute.value.query.gender }
});
// window.electronAPI.startProcess("LiveTalking", "LiveTalking.exe");
};
</script>

@ -90,7 +90,7 @@ import womenImg from "@/assets/live/girl.png";
import manImg from "@/assets/live/man.png";
import dyImg from "@/assets/live/dy.png";
import { useRouter } from "vue-router";
import { message } from "@/utils/message";
const showVideo1 = ref(false);
const showVideo2 = ref(false);
const dialogVisible = ref(false);
@ -104,8 +104,40 @@ const selectHuman = (type: string) => {
// dialogVisible.value = true;
router.push({ name: "AiLive", query: { gender: type } });
};
const validateProductInfo = async type => {
//
const productName = await window.electronAPI.getConfig(
"live_config",
"product_name"
);
const productSpecification = await window.electronAPI.getConfig(
"live_config",
"product_specification"
);
const productDescription = await window.electronAPI.getConfig(
"live_config",
"product_description"
);
//
const emptyFields = [];
if (!productName?.trim()) emptyFields.push("商品名称");
if (!productSpecification?.trim()) emptyFields.push("商品规格");
if (!productDescription?.trim()) emptyFields.push("商品描述");
//
if (emptyFields.length > 0) {
message(`以下字段不能为空: ${emptyFields.join("、")}`, { type: "error" });
selectHuman(type);
return false;
} else {
return true;
}
};
//
const enterLive = type => {
const enterLive = async type => {
const isValid = await validateProductInfo(type);
if (!isValid) return;
router.push({
path: "/Live",
query: { gender: type }

@ -11,16 +11,28 @@
<tipsImg />
<span>直播期间不能关闭页面页面关闭将自动停止直播</span>
</div>
<div>{{ `livetlking_enable_status ${livetlking_enable_status}` }}</div>
<div>{{ `gptsovits_enable_status ${gptsovits_enable_status}` }}</div>
<div>{{ `chat_enable_status ${chat_enable_status}` }}</div>
<!-- <div>{{ test }}</div> -->
<div class="title">直播弹幕</div>
<div v-if="isConnected" class="bullet-connect"></div>
<div v-else class="bullet-disconnect">未接管</div>
<div class="title">房间号</div>
<el-input v-model="liveRoom" style="margin-bottom: 16px" />
<el-input
:disabled="isStartConnectBullet || isConnected"
v-model="liveRoom"
style="margin-bottom: 16px"
/>
<el-button
type="primary"
v-if="!isConnected"
:loading="loading"
:disabled="isStartConnectBullet || !liveRoom"
type="primary"
@click="connectBullet"
>连接弹幕</el-button
>
{{ buttonText }}</el-button
>
<!-- <div @click="startPlay"></div> -->
</div>
@ -34,12 +46,11 @@
ref="videoRef"
autoplay
playsinline
muted
webkit-playsinline
controlslist="nodownload nofullscreen noremoteplayback"
class="video-player"
/>
<div v-show="isPlaying" class="close-btn">
<div v-show="isPlaying" @click="goback" class="close-btn">
<el-icon><SwitchButton /></el-icon>
<span>关闭直播</span>
</div>
@ -52,11 +63,12 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import tipsImg from "@/assets/svg/live/tips.svg";
import liveLoading from "@/assets/live/loading.png";
import liveLoading from "@/assets/live/loading.gif";
import { SwitchButton } from "@element-plus/icons-vue";
import { message } from "@/utils/message";
const title = ref("慧文的直播间");
const time = ref("00:00:00");
const loading = ref(false);
// ID
let timer = null;
//
@ -68,11 +80,13 @@ const router = useRouter();
//
const isConnected = ref(false);
const liveRoom = ref("");
const buttonText = ref("连接弹幕");
//
const isReady = ref(false);
const livetlking_enable_status = ref<any>("0");
const gptsovits_enable_status = ref<any>("0");
const chat_enable_status = ref<any>("0");
// const test = ref<any>("");
//
const isStartConnectBullet = ref(false);
//
@ -80,10 +94,10 @@ const isStartConnectBullet = ref(false);
// "http://192.168.10.209:8010/rtc/v1/whep/?app=live&stream=livestream";
let pc = null;
// ICE NAT 穿
const iceServers = [
{ urls: "stun:stun.l.google.com:19302" }, // STUN
{ urls: "stun:stun1.l.google.com:19302" }
];
// const iceServers = [
// { urls: "stun:stun.l.google.com:19302" }, // STUN
// { urls: "stun:stun1.l.google.com:19302" }
// ];
// HH:MM:SS
const formatTime = seconds => {
const h = Math.floor(seconds / 3600)
@ -115,7 +129,7 @@ const startPlay = async () => {
try {
statusText.value = "连接中...";
// 1. RTCPeerConnection
pc = new RTCPeerConnection({ iceServers });
pc = new RTCPeerConnection({});
// 2.
pc.ontrack = event => {
@ -128,47 +142,51 @@ const startPlay = async () => {
};
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });
return pc
.createOffer()
.then(offer => {
return pc.setLocalDescription(offer);
})
.then(() => {
// wait for ICE gathering to complete
return new Promise<void>(resolve => {
if (pc.iceGatheringState === "complete") {
resolve();
} else {
const checkState = () => {
if (pc.iceGatheringState === "complete") {
pc.removeEventListener("icegatheringstatechange", checkState);
resolve();
}
};
pc.addEventListener("icegatheringstatechange", checkState);
}
});
})
.then(() => {
const offer = pc.localDescription;
return fetch("http://192.168.10.209:8010/offer", {
body: JSON.stringify({
sdp: offer.sdp,
type: offer.type
}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
});
})
.then(response => {
return response.json();
})
.then(answer => {
return pc.setRemoteDescription(answer);
})
.catch(e => {});
return (
pc
.createOffer()
.then(offer => {
return pc.setLocalDescription(offer);
})
// .then(() => {
// test.value = "333333";
// wait for ICE gathering to complete
// return new Promise<void>(resolve => {
// test.value = "44444";
// if (pc.iceGatheringState === "complete") {
// resolve();
// } else {
// const checkState = () => {
// if (pc.iceGatheringState === "complete") {
// pc.removeEventListener("icegatheringstatechange", checkState);
// resolve();
// }
// };
// pc.addEventListener("icegatheringstatechange", checkState);
// }
// });
// })
.then(() => {
const offer = pc.localDescription;
return fetch("http://127.0.0.1:8010/offer", {
body: JSON.stringify({
sdp: offer.sdp,
type: offer.type
}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
});
})
.then(response => {
return response.json();
})
.then(answer => {
return pc.setRemoteDescription(answer);
})
.catch(e => {})
);
} catch (error) {
console.error("播放失败:", error);
}
@ -224,13 +242,15 @@ const getLiveTalkingResult = async () => {
);
};
const getIsConnectBullet = async () => {
livetlking_enable_status.value = await window.electronAPI.getConfig(
chat_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"chat_enable_status"
);
};
const connectBullet = async () => {
console.log("开始弹幕");
buttonText.value = "连接中...";
loading.value = true;
isStartConnectBullet.value = true;
await window.electronAPI.updateConfig(
"live_config",
@ -246,12 +266,13 @@ const connectBullet = async () => {
if (chat_enable_status.value === "1") {
isReady.value = true;
console.log("连接弹幕成功");
loading.value = false;
message("连接弹幕成功", { type: "success" });
isConnected.value = true;
clearInterval(checkInterval);
clearInterval(checkInterval); //
}
}, 1000);
}, 500);
};
const closedLive = () => {
window.electronAPI.updateConfig(
@ -299,7 +320,7 @@ onMounted(() => {
startLiveTimer();
clearInterval(checkInterval); //
}
}, 1000);
}, 500);
});
//
@ -308,9 +329,9 @@ onUnmounted(() => {
});
function enablePiP() {
// if (videoRef.value && document.pictureInPictureEnabled) {
// videoRef.value.requestPictureInPicture();
// }
if (videoRef.value && document.pictureInPictureEnabled) {
videoRef.value.requestPictureInPicture();
}
}
</script>
<style lang="scss" scoped>
@ -385,6 +406,15 @@ function enablePiP() {
margin-top: 12px;
}
.bullet-disconnect {
width: 72px;
height: 24px;
background: #e5e6eb;
border-radius: 2px 2px 2px 2px;
line-height: 24px;
text-align: center;
font-size: 14px;
}
.bullet-connect {
width: 72px;
height: 24px;
background: #e5e6eb;
@ -435,15 +465,17 @@ function enablePiP() {
height: 40px;
}
.video-loading {
background: #272e3b;
// background-image: url(../../assets/live/loading.gif);
// background-size: 100% 100%;
background: #010102;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
img {
width: 48px;
height: 48px;
width: 153px;
height: 133px;
margin-bottom: 16px;
}
span {

@ -89,7 +89,7 @@ onMounted(async () => {
<div class="left-content">
<div class="top">
<img :src="videoImg" alt="" />
<span>数字人直播</span>
<span>直播数字人</span>
</div>
<div class="desc">
{{ `AI生成式能力 | 智能衔接 | 高灵活 | 强创意 ` }}

Loading…
Cancel
Save