<script setup lang="ts"> import { onMounted, reactive, ref, nextTick } from "vue"; import { Sunny, UploadFilled, Delete, Download, Plus, ZoomIn, Files } from "@element-plus/icons-vue"; import { ElMessage, genFileId, type UploadInstance, type UploadProps, type UploadRawFile, type UploadFile } from "element-plus"; import text2videoService from "@/api/service/text2videoService"; import utils from "@/utils/utils"; import { useManyValues } from './compositions/useManyValues' const debug = ref(import.meta.env.MODE === 'production' ? false : true); // const debug = ref(false); const loading = ref(false); const dialogVisible = ref(false); const dialogData = ref(""); const default_data = useManyValues(); const form = reactive({ screen: default_data.screen, img_size: <Wm.ImgSize>{}, if_need_subtitle: default_data.if_need_subtitle, chatgpt_prompt: "", chatgpt_answer: "", chatgpt_answer_roles: <Wm.RolesItem[]>[], all_roles: "", adapt_result_json: <Wm.ScriptsItem[]>[], task_id: "", final_video: "", }); const sd_model = default_data.sd_paras.juggernautXL_v9Rdphoto2Lightning; const sd_prompt_prefix = sd_model.sd_prompt_prefix; const sd_negative_prompt_prefix = default_data.sd_negative_prompt_prefix; const wen_an_llm = default_data.llms.tyqw_online; const role_llm = default_data.llms.tyqw_online; const role_keywords_llm = default_data.llms.tyqw_online; const tuili_llm = default_data.llms.tyqw_online; const tuili_keyword_llm = default_data.llms.tyqw_online; // const fanyi_llm = default_data.llms.tyqw_online; const voice_rate = ref(0) const voice_volume = ref(0) const voice = ref("zh-CN-YunjianNeural") const bgm = ref("解忧曲") const bgm_volume = ref(0.3) const pwdCheckDialogVisible = ref(false); const pwdCheckValue = ref("") const sub_font_color = ref("#FFFF00") const sub_bg_color = ref() const sub_font_size = ref(35) const sub_position = ref(0.3) const marketingTemplateVisible = ref(false); const marketing_template = reactive({ product_name: "", product_description: "", target_people: "", text_role: "", text_style: "", story_type: "", reference: "", words_num: 0, prompt1: "", prompt2: "", result1: "", result2: "", }); const cover_backcover = reactive({ if_need_cover_pic: "false", cover_pic: "src/assets/waiting.png", cover_pic_local: "", cover_pic_with_text: "src/assets/waiting.png", cover_pic_with_text_local: "", cover_pic_titles: <Wm.PicText[]>[], cover_pic_use_scene: "", if_need_product_pic: "false", product_pic: "src/assets/waiting.png", product_pic_local: "", product_pic_with_text: "src/assets/waiting.png", product_pic_with_text_local: "", product_pic_titles: <Wm.PicText[]>[], product_pic_speech: "", }); const inPaintVisible = ref(false); const inPaintBaseImgData = ref(""); const inPaintMaskData = ref(""); const inPaintType = ref(""); const inPaintItem = ref(); const inPaintPrompt = ref(""); onMounted(() => { // 初始化task_id form.task_id = utils.genDateTimeStr(); console.log('页面加载,task_id=', form.task_id) // 初始化示例数据 onChangeScreen(form.screen); // 初始化密码框 if (debug.value == true) { pwdCheckDialogVisible.value = false; } else { pwdCheckDialogVisible.value = true; } }); const delay = (ms: any) => new Promise(res => setTimeout(res, ms)); const onSubmitGpt = () => { text2videoService .submitLLM(utils.aesEncrypt(form.chatgpt_prompt), utils.aesEncrypt(wen_an_llm.api), [], form.task_id, "true") .then((result: string) => { // console.log(form.chatgpt_prompt); // console.log(result); form.chatgpt_answer = result; }) .catch((error: any) => { // console.error(error); ElMessage({ message: error, type: "error", }); }); }; const onAdaptRoles = async () => { if (!form.chatgpt_answer || form.chatgpt_answer.length == 0) { ElMessage({ message: "文案不能为空", type: "error", }); return; } if (!form.task_id) { // 初始化task_id form.task_id = utils.genDateTimeStr(); console.log('推理角色,生成task_id=', form.task_id) } // 推理角色 try { form.chatgpt_answer_roles = []; const adapt_restrict = `请理解这个故事:“${form.chatgpt_answer}”,给出这个故事中的所有角色,多个角色以逗号分隔。\n要求:只返回角色名称即可,不要添加其他的内容。`; let roles = await text2videoService.submitLLM(utils.aesEncrypt(adapt_restrict), utils.aesEncrypt(role_llm.api), [], form.task_id, "true"); form.all_roles = roles.replace(/。|(|)/g, '').replace(/、/g, ','); console.log(form.all_roles); const roles_arr = form.all_roles.split(/[,,]/); console.log(roles_arr); // 推理属性 const attribute_options = default_data.role_attribute_options.map(option => option.value); console.log('dddddddd', attribute_options) for (const one_role of roles_arr) { // const adapt_attribute_restrict = `根据这个故事:“${form.chatgpt_answer}”,你认为这个角色:“${one_role}”是“${String(attribute_options)}”中的哪一种?请返回选择的结果。\n要求:只返回选择的结果即可,不要添加其他的内容。`; // const adapt_attribute_restrict = `你认为这个角色:“${one_role}”是“${String(attribute_options)}”中的哪一种?请返回选择的结果。\n要求:只返回选择的结果即可,不要添加其他的内容。`; const adapt_attribute_restrict = `故事全文:“${form.chatgpt_answer}”,请理解故事全文,然后判断:“${one_role}”是“${String(attribute_options)}”中的哪一种?请返回选择的结果。\n要求:只返回选择的结果即可,不要添加其他的内容。`; let attribute = await text2videoService.submitLLM(utils.aesEncrypt(adapt_attribute_restrict), utils.aesEncrypt(role_llm.api), [], form.task_id, "true"); console.log('dddddddd', attribute) if (attribute_options.includes(attribute)) { form.chatgpt_answer_roles.push({ "角色": one_role.trim(), "角色关键词": "", "角色关键词英文": "", "属性": attribute, }); } } console.log(form.chatgpt_answer_roles) } catch (error) { ElMessage({ message: String(error), type: "error", }); } }; const onAdaptRolesKeywords = async () => { if (!form.chatgpt_answer_roles || form.chatgpt_answer_roles.length == 0) { ElMessage({ message: "总角色不能为空", type: "error", }); return; } for (const one_role of form.chatgpt_answer_roles) { if (!one_role.属性) { ElMessage({ message: `请选择 ${one_role.角色} 的属性`, type: "error", }); return; } } loading.value = true; // 推理角色关键词 try { async function processRoles() { for (const one_role of form.chatgpt_answer_roles) { await delay(100); let adapt_keyword_restrict = `请理解这个故事:“${form.chatgpt_answer}”,给出其中这个角色“${one_role.角色}”的关键词,以逗号分隔。包括但不限于:`; let temp_restrict = ""; if (one_role.属性.includes("人")) { temp_restrict = `性别(可以发挥想象进行补充,但一定要有), 年龄(可以发挥想象进行补充,但一定要有), 肤色(可以发挥想象进行补充,但一定要有), 衣服(可以发挥想象进行补充,但一定要有), 发型(可以发挥想象进行补充,但一定要有), 发色(可以发挥想象进行补充,但一定要有), 脸色(可以发挥想象进行补充,但一定要有), 五官特点(可以发挥想象进行补充,但一定要有)`; } else { let temp_restrict_pre = ""; if (one_role.属性.includes("动物")) { temp_restrict_pre = `哪一种动物(可以发挥想象进行补充,但一定要有),` } temp_restrict = `${temp_restrict_pre} 体型(可以发挥想象进行补充,但一定要有), 颜色(可以发挥想象进行补充,但一定要有), 四肢(可以发挥想象进行补充,但一定要有), 五官特点(可以发挥想象进行补充,但一定要有), 身体(皮毛,羽毛,鳞片,肤色等,可以发挥想象进行补充,没有就不提供)`; } adapt_keyword_restrict = adapt_keyword_restrict + temp_restrict + "。回答限制在30个字左右。"; let keywords = await text2videoService.submitLLM(utils.aesEncrypt(adapt_keyword_restrict), utils.aesEncrypt(role_keywords_llm.api), [], form.task_id, "true"); keywords = keywords.replace(/。/g, '').replace(/、/g, ','); one_role.角色关键词 = keywords; await delay(100); // const adapt_attribute_restrict_en = `你现在扮演专业的英语翻译的角色。请将这段文字“${keywords}”翻译为英语。\n要求:只返回英语即可,不要返回其他内容。`; // let keywords_en = await text2videoService.submitLLM(adapt_attribute_restrict_en, role_keywords_llm.api); let keywords_en = await text2videoService.submitTranslateToEn(utils.aesEncrypt(keywords), form.task_id, "true"); // console.log(keywords_en) // keywords_en = utils.filterChineseAndPunctuation(keywords_en.replace(/"/g, '')); keywords_en = keywords_en.replace(/"/g, ''); one_role.角色关键词英文 = keywords_en; } } try { await processRoles(); console.log(form.chatgpt_answer_roles) } catch (error) { ElMessage({ message: String(error), type: "error" }); } } catch (error) { ElMessage({ message: String(error), type: "error", }); } finally { // 最终关闭loading(无论成功或失败) loading.value = false; } }; const onAdapt = async (type: string) => { if (!form.chatgpt_answer || form.chatgpt_answer.length == 0) { ElMessage({ message: "文案不能为空", type: "error", }); return; } if (!form.task_id) { // 初始化task_id form.task_id = utils.genDateTimeStr(); console.log('分镜,生成task_id=', form.task_id) } // 按标点拆分成分镜 const sentences = utils.splitText(form.chatgpt_answer, type); // console.log(sentences.length) // 分镜 form.adapt_result_json = [] for (let i = 0; i < sentences.length; i++) { form.adapt_result_json.push({ "编号": (i + 1).toString(), "场景描述": sentences[i].trim(), "场景关键词": "", "场景关键词英文": "", "角色": "", "角色关键词": "", "角色关键词英文": "", "画面描述词": "", "本镜配图": "src/assets/waiting.png", "local_image_path": "", "info": "", "roles": [], }); } console.log(form.adapt_result_json) } const onAdaptScene = async () => { if (!form.adapt_result_json || form.adapt_result_json.length == 0) { ElMessage({ message: "分镜不能为空", type: "error", }); return; } loading.value = true; async function processScenes() { for (const item of form.adapt_result_json) { await onAdaptOneScene(item); } } try { await processScenes(); ElMessage({ message: "all scene ok", type: "success" }); console.log(form.adapt_result_json); } catch (error) { ElMessage({ message: String(error), type: "error" }); } finally { loading.value = false; // 最终关闭loading(无论成功或失败) } }; const onAdaptOneScene = async (item: any) => { if (!item.场景描述) { ElMessage({ message: `分镜 ${item.编号} 场景描述不能为空`, type: "error", }); return; } // 推理场景 // 人物(可以发挥想象进行补充,但一定要有), 暂时去掉 try { // const adapt_restrict = `故事:\n${form.chatgpt_answer}\n // 指令: // 请理解这个故事,给出这个场景“${item.场景描述}”的关键词(年代(可以发挥想象进行补充,但一定要有),空间(可以发挥想象进行补充,但一定要有), // 时间段(可以发挥想象进行补充,但一定要有),地理环境(可以发挥想象进行补充,但一定要有),天气(可以发挥想象进行补充,但一定要有), // 物品(可以发挥想象进行补充,但一定要有),镜头角度(可以发挥想象进行补充,但一定要有))。 // 要求: // 关键词以逗号分隔。 // 只要返回关键词,不需要其他的说明文字。`; // const adapt_restrict = `剧本:“${form.chatgpt_answer}” \n 作为资深作家,请阅读剧本,并严格按照以下表单: // [{"旁白":"${item.场景描述}",场景描述:""}] // 根据旁白,来构造场景描述,语言要简要和清晰。你填写的内容只包括根据剧本所构思的摄像机镜头内的场景描述,而不需要其他的内容。请将你完成的表单返回给我。`; const adapt_restrict = `剧本:“${form.chatgpt_answer}” 指令: 作为资深作家,请阅读这个剧本,给出这个场景:“${item.场景描述}”的描述,50个字左右。`; const keywords = await text2videoService.submitLLM(utils.aesEncrypt(adapt_restrict), utils.aesEncrypt(tuili_llm.api), [], form.task_id, "true"); // console.log(adapt_restrict) // console.log(keywords) item.场景关键词 = keywords; // const adapt_restrict_en = `你现在扮演专业的英语翻译的角色。请将这段文字“${item.场景描述}”翻译为英语。\n要求:只返回英语即可,不要返回其他内容。`; // const keywords_en = await text2videoService.submitLLM(adapt_restrict_en, tuili_llm.api); let keywords_en = await text2videoService.submitTranslateToEn(utils.aesEncrypt(keywords), form.task_id, "true"); // console.log(keywords_en) // item.场景关键词英文 = utils.filterChineseAndPunctuation(keywords_en.replace(/"/g, '')); item.场景关键词英文 = keywords_en.replace(/"/g, ''); } catch (error) { ElMessage({ message: String(error), type: "error", }); } }; const onAdaptSceneRoles = async () => { if (!form.adapt_result_json || form.adapt_result_json.length == 0) { ElMessage({ message: "分镜不能为空", type: "error", }); return; } if (!form.all_roles) { ElMessage({ message: `总角色不能为空`, type: "error", }); return; } loading.value = true; async function processScenes() { for (const item of form.adapt_result_json) { await onAdaptOneSceneRoles(item); } } try { await processScenes(); console.log(form.adapt_result_json); } catch (error) { ElMessage({ message: String(error), type: "error" }); } finally { loading.value = false; // 最终关闭loading(无论成功或失败) } }; const onAdaptOneSceneRoles = async (item: any) => { if (!item.场景描述) { ElMessage({ message: `分镜 ${item.编号} 场景描述不能为空`, type: "error", }); return; } if (!form.all_roles) { ElMessage({ message: `总角色不能为空`, type: "error", }); return; } // 推理角色 try { if (form.chatgpt_answer_roles.length === 0) { // 总角色为空 item.角色 = ''; item.角色关键词 = ''; } else { // 总角色不为空 // const adapt_role_restrict = `请理解这个故事:“${form.chatgpt_answer}”,针对其中的这个场景:“${item.场景描述}”,从所有角色:“${form.all_roles}”中选择本场景的角色,多个角色以逗号分隔。`; const adapt_role_restrict = `整个故事(“${form.chatgpt_answer}),\n\n本章节(${item.场景描述}),\n\n角色列表(${form.all_roles}),\n\n请返回给本章节出现的角色。注意:不要发挥想象,必须从角色列表中选出本章节出现过的角色,如果多个用逗号隔开。`; const item_roles_answer = await text2videoService.submitLLM(utils.aesEncrypt(adapt_role_restrict), utils.aesEncrypt(tuili_keyword_llm.api), [], form.task_id, "true"); console.log(`==============${item.编号}===============`) console.log('form.all_roles = ', form.all_roles) item.info = `推理返回:${item_roles_answer}` let item_roles_answer_list = item_roles_answer.trim().split(/[,,、]/); item_roles_answer_list = item_roles_answer_list.map(item => item.trim()); // 过滤推理出来的角色,都要在总角色里 item.roles = ['无角色']; for (const y of form.chatgpt_answer_roles) { item.roles.push(y.角色); } item.角色 = '无角色'; let filter_result = ''; console.log('item_roles_answer_list = ', item_roles_answer_list) for (const x of item_roles_answer_list) { for (const y of form.chatgpt_answer_roles) { if (x == y.角色) { filter_result += x+','; console.log('filter_result = ', filter_result) // 下拉列表添加项目 // item.roles.push(x); } } } if (filter_result.endsWith(',')) { filter_result = filter_result.slice(0, -1); // 去掉结尾的逗号 } if (filter_result) {item.角色 = filter_result;} console.log('过滤后 item.角色 = ', item.角色) ////// 加一段逻辑,本镜角色有且只能有一个,且尽量不与之前相同。【begin】 const temp_arr = item.角色.split(/[,,、]/); const temp_arr_length = temp_arr.length; ////// 如果本镜的角色大于1个,则只保留没有出现过的角色,且保证只有一个 if (temp_arr_length > 1) { ////// 获取本镜之前的所有角色 let role_history = ""; form.adapt_result_json.forEach(scene => { if (Number(scene.编号) < Number(item.编号)) { role_history += scene.角色 + ","; } }) role_history = role_history.replace(/,+$/, ''); console.log(`role_history = ${role_history}`) for (const temp of temp_arr) { if (role_history.includes(temp)) { item.角色 = ""; continue; } else { item.角色 = temp; break; } } if (!item.角色) { const role_history_arr = role_history.split(','); const lastOne = role_history_arr[role_history_arr.length - 1]; const remainingValues = role_history_arr.filter(i => i !== lastOne); if (remainingValues.length >= 1) { item.角色 = remainingValues[remainingValues.length - 1]; } else { item.角色 = item_roles_answer.trim(); } console.log(`lalalalala item.角色 = ${item.角色}`) } } console.log(`item.角色 = ${item.角色}`) ////// 加一段逻辑,本镜角色有且只能有一个,且尽量不与之前相同。【end】 // 开始匹配角色关键词 let role_kws = "" let role_kws_en = "" const item_roles_arr = item.角色.split(/[,,、]/); item_roles_arr.forEach((one_item_role: string) => { let temp_role_kws = "" let temp_role_kws_en = "" // 人工匹配角色关键词,先找想同的 for (const i of form.chatgpt_answer_roles) { if (i["角色"].trim() == one_item_role.trim()) { temp_role_kws = `${i["角色关键词"]}`; temp_role_kws_en = `${i["角色关键词英文"]}`; // 找到就ok break; } } // 如果找不到相同的,则模糊匹配 if (!temp_role_kws) { for (const i of form.chatgpt_answer_roles) { if (i["角色"].includes(one_item_role.trim()) || one_item_role.includes(i["角色"].trim())) { temp_role_kws = `${i["角色关键词"]}`; temp_role_kws_en = `${i["角色关键词英文"]}`; // 匹配到一个就ok break; } } } role_kws = `${role_kws}${temp_role_kws}`; role_kws_en = `${role_kws_en}${temp_role_kws_en}`; }) item.角色关键词 = role_kws; item.角色关键词英文 = role_kws_en; } } catch (error) { ElMessage({ message: String(error), type: "error", }); } }; const onItemRolesChange = (item: any) => { console.log(item.编号, item.角色); if (item.角色 == "无角色") { item.角色关键词 = ""; item.角色关键词英文 = ""; } else { for (const y of form.chatgpt_answer_roles) { if (item.角色 == y.角色) { item.角色关键词 = y.角色关键词; item.角色关键词英文 = y.角色关键词英文; break; } } } } const onDraw = async () => { if (!form.adapt_result_json || form.adapt_result_json.length == 0) { ElMessage({ message: "分镜不能为空", type: "error", }); return; } let is_all_ok = true; for (const item of form.adapt_result_json) { if (!item.场景关键词 && !item.角色关键词) { ElMessage({ message: `分镜 ${item.编号} 关键词为空,请重新推理本镜`, type: "error", }); is_all_ok = false; break; } }; if (is_all_ok) { for (const item of form.adapt_result_json) { // onDrawOne(item); // 因3090显存不够,暂时改成串行画图 await onDrawOne(item); } } }; const onDrawOne = async (item: any) => { if (!item.场景关键词 && !item.角色关键词) { ElMessage({ message: "本镜关键词为空,请重新推理本镜", type: "error", }); return; } if (!form.task_id) { ElMessage({ message: "task_id不能为空,请刷新页面", type: "error", }); return; } // 翻译+画图 try { item.本镜配图 = "src/assets/loading.gif"; // let temp_prompt = "" // if (item.场景描述) {temp_prompt = temp_prompt + `场景描述为:${item.场景描述}\n`}; // if (item.场景关键词) {temp_prompt = temp_prompt + `场景关键词为:${item.场景关键词}\n`}; // if (item.角色) {temp_prompt = temp_prompt + `场景中的角色有:${item.角色}\n`}; // if (item.角色关键词) {temp_prompt = temp_prompt + `角色关键词为:${item.角色关键词}\n`}; // const sd_describe = await text2videoService.submitLLM( // `${temp_prompt} // 指令: // 请理解以上内容,并返回一段英文的描述。`, fanyi_llm.api // ); // item.画面描述词 = sd_describe; item.画面描述词 = item.场景关键词英文 + "," + item.角色关键词英文; const sd_prompt = item.画面描述词 + "," + sd_prompt_prefix; // console.log(sd_prompt); // console.log(sd_negative_prompt_prefix); const sampler_index = sd_model.sampler_index; const seed = sd_model.seed; const steps = sd_model.steps; const cfg_scale = sd_model.cfg_scale; const model = sd_model.model; const sd_img = await text2videoService.submitSD( form.task_id, item.编号, utils.aesEncrypt(sd_prompt), utils.aesEncrypt(sd_negative_prompt_prefix), utils.aesEncrypt(form.img_size.width), utils.aesEncrypt(form.img_size.height), utils.aesEncrypt(sampler_index), utils.aesEncrypt(seed), utils.aesEncrypt(steps), utils.aesEncrypt(cfg_scale), "true", utils.aesEncrypt(model), ); item.本镜配图 = sd_img.domain_image_path + "?v=" + utils.genDateTimeStr(); item.local_image_path = sd_img.local_image_path; } catch (error) { ElMessage({ message: String(error), type: "error", }); item.本镜配图 = ""; item.local_image_path = ""; } }; const onGenVideo = async () => { if (!form.adapt_result_json || form.adapt_result_json.length == 0) { ElMessage({ message: "分镜必要信息不能为空,请重新执行", type: "error", }); return; } let is_all_ok = true; form.adapt_result_json.map(item => { if (item.编号 == "" || item.场景描述 == "" || item.local_image_path == "") { ElMessage({ message: `分镜 ${item.编号} 的必要信息为空,请重新执行`, type: "error", }); is_all_ok = false; } }); if (!is_all_ok) return; try { if (cover_backcover.if_need_cover_pic == 'true') { console.log("添加封面文字到图片") let res = await onMarketingTemplateAddTextToPic('cover'); if (!res) {return;} } if (cover_backcover.if_need_product_pic == 'true') { console.log("添加封底文字到图片") let res = await onMarketingTemplateAddTextToPic('product'); if (!res) {return;} } console.log(form.adapt_result_json); let video_param_detail = []; // 封面 if (cover_backcover.if_need_cover_pic == 'true' && cover_backcover.cover_pic_with_text_local) { video_param_detail.push({ idx: "0", text: "", img_path: cover_backcover.cover_pic_with_text_local, no_text_duration: "0.1" }) }; // 分镜 form.adapt_result_json.map(item => { video_param_detail.push({ idx: item.编号, text: item.场景描述, img_path: item.local_image_path }) }); // 封底 if (cover_backcover.if_need_product_pic == 'true' && cover_backcover.product_pic_with_text_local) { // 如果text没有值,则时长默认5秒 video_param_detail.push({ idx: String(form.adapt_result_json.length + 1), text: cover_backcover.product_pic_speech, img_path: cover_backcover.product_pic_with_text_local, no_text_duration: "5" }) }; let para_rate = `${voice_rate.value}%`; let para_volume = `${voice_volume.value}%`; if (voice_rate.value >= 0) { para_rate = `+${para_rate}` } if (voice_volume.value >= 0) { para_volume = `+${para_volume}` } const video_param = { task_id: form.task_id, if_need_subtitle: form.if_need_subtitle, lang: "zh", task_info: video_param_detail, rate: para_rate, volume: para_volume, voice: voice.value, bgm: bgm.value, bgm_volume: bgm_volume.value, sub_font_size: String(sub_font_size.value), sub_font_color: sub_font_color.value, sub_position: String(1 - sub_position.value), sub_bg_color: "", } if (sub_bg_color.value) { video_param.sub_bg_color = sub_bg_color.value; } const result = await text2videoService.submitGenVideo(video_param); console.log(result); form.final_video = ""; form.final_video = result + "?v=" + utils.genDateTimeStr(); } catch(error: any) { // console.error(error); ElMessage({ message: error, type: "error", }); }; }; const clean_data = () => { form.chatgpt_prompt = ""; form.chatgpt_answer = ""; form.chatgpt_answer_roles = <Wm.RolesItem[]>[]; form.adapt_result_json = <Wm.ScriptsItem[]>[]; form.task_id = ""; form.final_video = ""; cover_backcover.if_need_cover_pic = "false"; cover_backcover.cover_pic = "src/assets/waiting.png"; cover_backcover.cover_pic_local = ""; cover_backcover.cover_pic_with_text = "src/assets/waiting.png"; cover_backcover.cover_pic_with_text_local = ""; cover_backcover.cover_pic_titles = <Wm.PicText[]>[]; cover_backcover.cover_pic_use_scene = ""; cover_backcover.if_need_product_pic = "false"; cover_backcover.product_pic = "src/assets/waiting.png"; cover_backcover.product_pic_local = ""; cover_backcover.product_pic_with_text = "src/assets/waiting.png"; cover_backcover.product_pic_with_text_local = ""; cover_backcover.product_pic_titles = <Wm.PicText[]>[]; cover_backcover.product_pic_speech = ""; } const clean_roles = () => { form.chatgpt_answer_roles = <Wm.RolesItem[]>[]; } const clean_scenes = () => { form.adapt_result_json = <Wm.ScriptsItem[]>[]; } const onChangeScreen = (val: string) => { // 先清理数据 clean_data(); if (!form.task_id) { // 初始化task_id form.task_id = utils.genDateTimeStr(); console.log('更改屏幕设置,task_id=', form.task_id) } if (debug.value == true) { if (val == "横屏") { form.task_id = default_data.horizontal_data.task_id; form.chatgpt_prompt = default_data.horizontal_data.chatgpt_prompt; form.chatgpt_answer = default_data.horizontal_data.chatgpt_answer; form.chatgpt_answer_roles = default_data.horizontal_data.chatgpt_answer_roles; form.all_roles = default_data.horizontal_data.all_roles; form.adapt_result_json = default_data.horizontal_data.adapt_result_json; form.final_video = default_data.horizontal_data.final_video; } else { form.task_id = default_data.vertical_data.task_id; form.chatgpt_prompt = default_data.vertical_data.chatgpt_prompt; form.chatgpt_answer = default_data.vertical_data.chatgpt_answer; form.chatgpt_answer_roles = default_data.vertical_data.chatgpt_answer_roles; form.all_roles = default_data.vertical_data.all_roles; form.adapt_result_json = default_data.vertical_data.adapt_result_json; form.final_video = default_data.vertical_data.final_video; } marketing_template.product_name = default_data.marketing_template.product_name; marketing_template.product_description = default_data.marketing_template.product_description; marketing_template.target_people = default_data.marketing_template.target_people; marketing_template.text_role = default_data.marketing_template.text_role; marketing_template.text_style = default_data.marketing_template.text_style; marketing_template.story_type = default_data.marketing_template.story_type; marketing_template.reference = default_data.marketing_template.reference; marketing_template.words_num = default_data.marketing_template.words_num; // cover_backcover.if_need_product_pic = 'true'; // cover_backcover.if_need_cover_pic = 'true'; cover_backcover.product_pic_titles = default_data.cover_backcover.product_pic_titles; cover_backcover.product_pic_speech = default_data.cover_backcover.product_pic_speech; cover_backcover.cover_pic_titles = default_data.cover_backcover.cover_pic_titles; // cover_backcover.product_pic = default_data.cover_backcover.product_pic; // cover_backcover.product_pic_local = default_data.cover_backcover.product_pic_local; // cover_backcover.product_pic_with_text = default_data.cover_backcover.product_pic_with_text; // cover_backcover.product_pic_with_text_local = default_data.cover_backcover.product_pic_with_text_local; // cover_backcover.cover_pic = default_data.cover_backcover.cover_pic; // cover_backcover.cover_pic_local = default_data.cover_backcover.cover_pic_local; // cover_backcover.cover_pic_with_text = default_data.cover_backcover.cover_pic_with_text; // cover_backcover.cover_pic_with_text_local = default_data.cover_backcover.cover_pic_with_text_local; } // 宽高 if (val == "横屏") { form.img_size = default_data.horizontal_img_size; } else { form.img_size = default_data.vertical_img_size; } } const showsdprompt = (item: any) => { if (!item.画面描述词) { ElMessage({ message: "还未绘图,请绘图后查看", type: "error", }); return; } // alert(item.画面描述词) dialogData.value = `${item.画面描述词},${sd_prompt_prefix}===== 反向提示词 =====${sd_negative_prompt_prefix}`; dialogVisible.value = true; // 打开对话框 } const upload = ref<UploadInstance>() const actionUrl = ref( import.meta.env.MODE === 'production' ? '/file/upload_pic_and_modify' : import.meta.env.VITE_APP_BASE_API + '/file/upload_pic_and_modify' ) // 在分镜自定义上传图片时发现个bug // 第一次上传没问题,但重复进行上传,会更新到最后一行上去。 // 奇怪,没找到原因。暂时先通过迂回的方法来处理。 // 终于找到原因了,是handleUploadExceed的问题,它在超出limit后,清空已上传,并将新文件上传,但用的都是upload实例 let uploadItemId = 0 const onClickUpload=(val:any)=>{ uploadItemId = val.编号 - 1 console.log(uploadItemId); } const handleUploadSuccess = (val: Wm.UploadResult) => { console.log(val) if (val.code == 0) { form.adapt_result_json[uploadItemId].本镜配图 = val.data[0].url + "?v=" + utils.genDateTimeStr(); form.adapt_result_json[uploadItemId].local_image_path = val.data[0].path; ElMessage({ message: '上传成功', type: 'success' }) } else { ElMessage({ message: '上传失败:'+val.message, type: 'error' }) } } const handleUploadExceed: UploadProps['onExceed'] = (files) => { // 清除已上传的文件 upload.value!.clearFiles() // 获取超出限制的第一个文件 const file = files[0] as UploadRawFile // 给文件分配一个新的唯一标识 file.uid = genFileId() // 手动触发文件上传 upload.value!.handleStart(file) // 提交上传 upload.value!.submit() } const handleUploadError = (error: Error) => { ElMessage({ message: String(error.message), type: "error", }); } const handleBeforeUpload = async (file: any) => { const isLt1M = file.size / 1024 / 1024 <= 1; if (!isLt1M) { ElMessage.error('上传图片大小不能超过 1MB!') return false } return new Promise((resolve, reject) => { let is_size_ok = false; const reader = new FileReader(); reader.onload = (e:any) => { const dataURL = e.target.result; const img = new Image(); img.onload = () => { console.log('文件宽度:', img.width); console.log('文件高度:', img.height); if ((form.screen == '竖屏' && img.height >= img.width) || (form.screen == '横屏' && img.width >= img.height)) { is_size_ok = true; } if (is_size_ok) { resolve(true); // 尺寸符合要求 } else { reject('竖屏请上传竖屏图片,横屏请上传横屏图片!'); } }; img.src = dataURL; }; reader.readAsDataURL(file); }).catch(error => { console.log('Error:', error); ElMessage.error(error); return false; }); } const onClearOnePic = (item: any) => { item.本镜配图 = ""; item.local_image_path = ""; }; const onPwdCheckDialog = () => { text2videoService .submitPwdCheck(pwdCheckValue.value) .then((result: string) => { if (result == "success") { pwdCheckDialogVisible.value = false; } else { ElMessage({ message: result, type: "error", }); } }) .catch((error: any) => { ElMessage({ message: error, type: "error", }); }); } const onDeleteOne = (item: any) => { try { let delete_no = item.编号; // 删除记录 form.adapt_result_json = form.adapt_result_json.filter(item => item.编号 !== delete_no); // 重新对记录进行编号 form.adapt_result_json = form.adapt_result_json.map((item, index) => { return { ...item, 编号: (index + 1).toString() }; }); } catch (error) { ElMessage({ message: String(error), type: "error", }); } }; // /////// 模板相关 //////////// // const activeTab = ref("first"); // const goToNextPage = () => { // activeTab.value = "second"; // }; const upload_cover = ref<UploadInstance>() const upload_product = ref<UploadInstance>() const MarketingTemplateUploadProductPicSuccess = (val: Wm.UploadResult) => { // console.log(val) if (val.code == 0) { cover_backcover.product_pic = val.data[0].url + "?v=" + utils.genDateTimeStr(); cover_backcover.product_pic_local = val.data[0].path; cover_backcover.product_pic_with_text = 'src/assets/waiting.png'; cover_backcover.product_pic_with_text_local = ''; ElMessage({ message: '上传成功', type: 'success' }) // onMarketingTemplateAddTextToPic('product'); } else { ElMessage({ message: '上传失败:'+val.message, type: 'error' }) } } const handleMarketingTemplateUploadProductPicExceed: UploadProps['onExceed'] = (files) => { // 清除已上传的文件 upload_product.value!.clearFiles() // 获取超出限制的第一个文件 const file = files[0] as UploadRawFile // 给文件分配一个新的唯一标识 file.uid = genFileId() // 手动触发文件上传 upload_product.value!.handleStart(file) // 提交上传 upload_product.value!.submit() } const MarketingTemplateUploadCoverPicSuccess = (val: Wm.UploadResult) => { if (val.code == 0) { cover_backcover.cover_pic = val.data[0].url + "?v=" + utils.genDateTimeStr(); cover_backcover.cover_pic_local = val.data[0].path; cover_backcover.cover_pic_with_text = 'src/assets/waiting.png'; cover_backcover.cover_pic_with_text_local = ''; ElMessage({ message: '上传成功', type: 'success' }) // onMarketingTemplateAddTextToPic('cover'); } else { ElMessage({ message: '上传失败:'+val.message, type: 'error' }) } } const handleMarketingTemplateUploadCoverPicExceed: UploadProps['onExceed'] = (files) => { // 清除已上传的文件 upload_cover.value!.clearFiles() // 获取超出限制的第一个文件 const file = files[0] as UploadRawFile // 给文件分配一个新的唯一标识 file.uid = genFileId() // 手动触发文件上传 upload_cover.value!.handleStart(file) // 提交上传 upload_cover.value!.submit() } const onClearMarketingTemplatePic = (type: string) => { if (type == 'product') { cover_backcover.product_pic = 'src/assets/waiting.png'; cover_backcover.product_pic_local = ''; cover_backcover.product_pic_with_text = 'src/assets/waiting.png'; cover_backcover.product_pic_with_text_local = ''; }; if (type == 'cover') { cover_backcover.cover_pic = 'src/assets/waiting.png'; cover_backcover.cover_pic_local = ''; cover_backcover.cover_pic_with_text = 'src/assets/waiting.png'; cover_backcover.cover_pic_with_text_local = ''; cover_backcover.cover_pic_use_scene = ''; }; }; const check_if_has_pic = (type: string) => { if ((type == 'product' && !cover_backcover.product_pic_local) || (type == 'cover' && !cover_backcover.cover_pic_local)) { ElMessage({ message: '请先设置图片', type: 'error' }) return false; }; return true; }; const onAddMarketingTemplatePicText = async (type: string, action: string, index: number) => { if (action == "add") { if(!check_if_has_pic(type)) {return}; if (type == 'cover') { const newElement = { text: "新的文本", color: "#000000", bg_color: "#FFFFFF", font_size: 60, position: 0.37 }; cover_backcover.cover_pic_titles.push(newElement); }; if (type == 'product') { const newElement = { text: "关注 点赞 评论", color: "#DAFB01", bg_color: "#000000", font_size: 60, position: 0.2 }; cover_backcover.product_pic_titles.push(newElement); }; }; if (action == "del") { if (type == 'cover') {cover_backcover.cover_pic_titles.splice(index, 1)}; if (type == 'product') {cover_backcover.product_pic_titles.splice(index, 1)}; }; await onMarketingTemplateAddTextToPic(type); }; const onMarketingTemplateAddTextToPic = async (type: string) => { if ((type == 'product' && !cover_backcover.product_pic_local) || (type == 'cover' && !cover_backcover.cover_pic_local)) { ElMessage({ message: '请先设置图片', type: 'error' }) return false; }; if (type == 'product' && cover_backcover.product_pic_titles.length==0) { cover_backcover.product_pic_with_text = cover_backcover.product_pic; cover_backcover.product_pic_with_text_local = cover_backcover.product_pic_local; return true; }; if (type == 'cover' && cover_backcover.cover_pic_titles.length==0) { cover_backcover.cover_pic_with_text = cover_backcover.cover_pic; cover_backcover.cover_pic_with_text_local = cover_backcover.cover_pic_local; return true; }; let params = { image_path: '', image_texts: [{ text: '', text_color: '', text_bg_color: '', font_size: '', position: '', lang: '', }], }; if (type == 'product') { const texts = cover_backcover.product_pic_titles.map(item => { return { text: item.text, text_color: item.color ? item.color : '', text_bg_color: item.bg_color ? item.bg_color : '', font_size: String(item.font_size), position: String(1 - item.position), lang: 'zh', }; }); params = { image_path: cover_backcover.product_pic_local, image_texts: texts, }; }; if (type == 'cover') { const texts = cover_backcover.cover_pic_titles.map(item => { return { text: item.text, text_color: item.color ? item.color : '', text_bg_color: item.bg_color ? item.bg_color : '', font_size: String(item.font_size), position: String(1 - item.position), lang: 'zh', }; }); params = { image_path: cover_backcover.cover_pic_local, image_texts: texts, }; }; params.image_texts.forEach((item, index) => { if (!item.text_color || item.text_color.trim() === '') { ElMessage({ message: '字体颜色不能为空', type: 'error' }) return false; } }); try { const result = await text2videoService.submitAddTextToImg(params); // console.log(result); if (type == 'product') { cover_backcover.product_pic_with_text = result.domain_image_path + "?v=" + utils.genDateTimeStr(); cover_backcover.product_pic_with_text_local = result.local_image_path; } if (type == 'cover') { cover_backcover.cover_pic_with_text = result.domain_image_path + "?v=" + utils.genDateTimeStr(); cover_backcover.cover_pic_with_text_local = result.local_image_path; } return true; } catch (error) { ElMessage({ message: String(error), type: "error", }); if (type == 'product') { cover_backcover.product_pic_with_text = ""; cover_backcover.product_pic_with_text_local = ""; } if (type == 'cover') { cover_backcover.cover_pic_with_text = ""; cover_backcover.cover_pic_with_text_local = ""; } return false; } }; const onMarketingTemplateSubmitGpt = async () => { marketing_template.prompt1 = ` 商品名称:{${marketing_template.product_name}} 商品卖点:[${marketing_template.product_description}] 文案主角:{${marketing_template.text_role}} 产品目标群体:{${marketing_template.target_people}} 参考信息:[${marketing_template.reference}] 你是营销专家,请写一个文案主角和商品的互动文案,要先理解商品卖点,学习参考信息,以${marketing_template.text_style}的风格,先整理出这篇营销文案的文案提纲。 注意,要用精简的语言总结出文案的提纲,文案提纲是:{} 在最后总结,把故事情节总结成一句夺人眼球的疑问句开头,设置悬念,需要你总结成15个字以内,开头语总结是:{} ` marketing_template.prompt2 = ` 你是${marketing_template.story_type}作家,请根据原始内容和你分析的框架,写一个${marketing_template.words_num}字的${marketing_template.story_type}故事文案。 特别注意,不能提到功效类用词,不要直接用目标群体的描述带入到框架中,用这个群体的特征描述来代。第一句用你总结的开头语,整体字数是${marketing_template.words_num}字。 你给出的文案是:{} ` try { console.log("第一次提问") let result1 = await text2videoService.submitLLM(utils.aesEncrypt(marketing_template.prompt1), utils.aesEncrypt(wen_an_llm.api), [], form.task_id, "true"); marketing_template.result1 = result1; // console.log(marketing_template.prompt1); // console.log(marketing_template.result1); console.log("第二次带历史提问") const history = [ {'role': utils.aesEncrypt('user'), 'content': utils.aesEncrypt(marketing_template.prompt1)}, {'role': utils.aesEncrypt('assistant'), 'content': utils.aesEncrypt(result1)}] let result2 = await text2videoService.submitLLM(utils.aesEncrypt(marketing_template.prompt2), utils.aesEncrypt(wen_an_llm.api), history, form.task_id, "true"); marketing_template.result2 = result2; // console.log(JSON.stringify(history), marketing_template.prompt2); // console.log(marketing_template.result2); form.chatgpt_answer = result2; marketingTemplateVisible.value = false; } catch (error) { ElMessage({ message: String(error), type: "error", }); } }; const onTest = () => { console.log(form.chatgpt_prompt); const encrypt_data = utils.aesEncrypt(form.chatgpt_prompt); console.log(encrypt_data); text2videoService .submitTest(encrypt_data) .then((result: string) => { console.log(result); form.chatgpt_answer = result; }) .catch((error: any) => { // console.error(error); ElMessage({ message: error, type: "error", }); }); }; const onSelectCoverChange = (value: any) => { cover_backcover.cover_pic = form.adapt_result_json[parseInt(value)-1].本镜配图; cover_backcover.cover_pic_local = form.adapt_result_json[parseInt(value)-1].local_image_path; cover_backcover.cover_pic_with_text = 'src/assets/waiting.png'; cover_backcover.cover_pic_with_text_local = ''; onMarketingTemplateAddTextToPic('cover'); } // 局部重绘Dialog const showInPaintDialog = (type: string, item: any) => { let base_img_path = ""; inPaintType.value = type; inPaintItem.value = null; inPaintPrompt.value = ""; inPaintBaseImgData.value = ""; inPaintMaskData.value = ""; if (type == "scene") { inPaintItem.value = item; // inPaintPrompt.value = inPaintItem.value.场景关键词+' '+inPaintItem.value.角色关键词; inPaintPrompt.value = ""; base_img_path = inPaintItem.value.本镜配图; } else if (type == "cover") { inPaintPrompt.value = ""; base_img_path = cover_backcover.cover_pic; } else if (type == 'product') { inPaintPrompt.value = ""; base_img_path = cover_backcover.product_pic; }; if (!base_img_path || base_img_path.length==0) { ElMessage({ message: "没有基础图片,请确认", type: "error", }); return; } inPaintVisible.value = true; // 打开对话框 // 等待元素加载完成 nextTick(() => { // 局部重绘 const base_canvas = document.getElementById('baseCanvas') as HTMLCanvasElement; const mask_canvas = document.getElementById('maskCanvas') as HTMLCanvasElement; if (base_canvas && mask_canvas) { let base_ctx = base_canvas.getContext('2d') as CanvasRenderingContext2D; base_ctx.clearRect(0, 0, base_canvas.width, base_canvas.height); var img = new Image(); img.crossOrigin = 'Anonymous'; // 如果图片需要用于跨域,则需要设置这个属性 img.onload = function() { base_canvas.width = img.width; base_canvas.height = img.height; base_ctx.drawImage(img, 0, 0, base_canvas.width, base_canvas.height); }; img.src = base_img_path; let mask_ctx = mask_canvas.getContext('2d') as CanvasRenderingContext2D; mask_ctx.clearRect(0, 0, mask_canvas.width, mask_canvas.height); mask_ctx.lineWidth = 40; // 设置线条粗细 mask_ctx.strokeStyle = 'rgba(255, 255, 255, 1)'; // 设置线条颜色 mask_ctx.lineCap = 'round'; // 线头尾为圆形 mask_ctx.lineJoin = 'round'; // 拐点为圆形,默认是尖角 // mask_ctx.globalCompositeOperation = 'destination-atop'; //避免线条重叠的时候透明度也重叠 // mask_ctx.globalCompositeOperation = 'copy'; //避免线条重叠的时候透明度也重叠 let isDrawing = false; // 鼠标或触摸事件开始绘制 mask_canvas.addEventListener('mousedown', startDrawing); mask_canvas.addEventListener('touchstart', startDrawing); // 鼠标或触摸事件绘制中 mask_canvas.addEventListener('mousemove', draw); mask_canvas.addEventListener('touchmove', draw); // 鼠标或触摸事件结束绘制 mask_canvas.addEventListener('mouseup', stopDrawing); mask_canvas.addEventListener('touchend', stopDrawing); mask_canvas.addEventListener('mouseout', stopDrawing); // 清除按钮点击事件 const clearButton = document.getElementById('clearButton') as HTMLButtonElement; clearButton.addEventListener('click', clearCanvas); // 开始绘制 function startDrawing(e: any) { isDrawing = true; mask_ctx.beginPath(); const { offsetX, offsetY } = getOffset(e); mask_ctx.moveTo(offsetX, offsetY); draw(e); } // 绘制中 function draw(e: any) { if (!isDrawing) return; const { offsetX, offsetY } = getOffset(e); mask_ctx.lineTo(offsetX, offsetY); mask_ctx.stroke(); } // 结束绘制 function stopDrawing() { isDrawing = false; inPaintBaseImgData.value = base_canvas.toDataURL('image/png'); inPaintMaskData.value = mask_canvas.toDataURL('image/png'); } // 清除画布 function clearCanvas() { mask_ctx.clearRect(0, 0, mask_canvas.width, mask_canvas.height); } // 获取鼠标或触摸事件的偏移量 function getOffset(e: any) { const rect = mask_canvas.getBoundingClientRect(); let offsetX, offsetY; if (e.type.includes('touch')) { offsetX = e.touches[0].clientX - rect.left; offsetY = e.touches[0].clientY - rect.top; } else { offsetX = e.offsetX; offsetY = e.offsetY; } return { offsetX, offsetY }; } } else { console.error('Canvas element not found'); } }); } const onSubmitInPaint = async () => { if (!form.task_id) { ElMessage({ message: "task_id不能为空,请刷新页面", type: "error", }); return; } // console.log('333', inPaintBaseImgData.value) // console.log('333', inPaintMaskData.value) // return; if (!inPaintBaseImgData.value || !inPaintMaskData.value) { ElMessage({ message: "请在图片上涂抹需要保留的部分!", type: "error", }); return; } if (!inPaintPrompt.value || inPaintPrompt.value.length==0) { ElMessage({ message: "请填写画面描述!", type: "error", }); return; } const sampler_index = sd_model.sampler_index; const seed = sd_model.seed; const steps = sd_model.steps; const cfg_scale = sd_model.cfg_scale; const model = sd_model.model; const base_img = inPaintBaseImgData.value; const mask = inPaintMaskData.value; let sd_prompt = ""; let img_id = "" let item = null; if (inPaintType.value == "scene") { item = inPaintItem.value; item.本镜配图 = "src/assets/loading.gif"; img_id = item.编号; } else if (inPaintType.value == "cover") { img_id = 'cover'; cover_backcover.cover_pic = 'src/assets/waiting.png'; cover_backcover.cover_pic_local = ''; } else if (inPaintType.value == 'product') { img_id = 'product'; cover_backcover.product_pic = 'src/assets/waiting.png'; cover_backcover.product_pic_local = ''; }; try { let keywords_en = await text2videoService.submitTranslateToEn(utils.aesEncrypt(inPaintPrompt.value), form.task_id, "true"); sd_prompt = keywords_en.replace(/"/g, '') + "," + sd_prompt_prefix; // const sd_img = await text2videoService.submitSDInPaint( // form.task_id, // img_id, // sd_prompt, // sd_negative_prompt_prefix, // form.img_size.width, // form.img_size.height, // sampler_index, // seed, // steps, // cfg_scale, // "false", // model, // base_img, // mask, // ); const sd_img = await text2videoService.submitSDInPaint( form.task_id, img_id, utils.aesEncrypt(sd_prompt), utils.aesEncrypt(sd_negative_prompt_prefix), utils.aesEncrypt(form.img_size.width), utils.aesEncrypt(form.img_size.height), utils.aesEncrypt(sampler_index), utils.aesEncrypt(seed), utils.aesEncrypt(steps), utils.aesEncrypt(cfg_scale), "true", utils.aesEncrypt(model), utils.aesEncrypt(base_img), utils.aesEncrypt(mask), ); if (inPaintType.value == "scene") { item.本镜配图 = sd_img.domain_image_path + "?v=" + utils.genDateTimeStr(); item.local_image_path = sd_img.local_image_path; } else if (inPaintType.value == 'cover') { cover_backcover.cover_pic = sd_img.domain_image_path + "?v=" + utils.genDateTimeStr(); cover_backcover.cover_pic_local = sd_img.local_image_path; } else if (inPaintType.value == 'product') { cover_backcover.product_pic = sd_img.domain_image_path + "?v=" + utils.genDateTimeStr(); cover_backcover.product_pic_local = sd_img.local_image_path; }; } catch (error) { ElMessage({ message: String(error), type: "error", }); } finally { inPaintVisible.value = false; // 关闭对话框 } } </script> <template> <main class="home-container"> <!-- 标题 --> <el-divider content-position="left">text2video</el-divider> <el-form :model="form" label-width="114px" v-loading="loading"> <el-form-item> <div> <el-radio-group v-model="form.screen" @change="onChangeScreen"> <el-radio label="横屏" size="large" border /> <el-radio label="竖屏" size="large" border /> </el-radio-group> </div> </el-form-item> <el-form-item> <el-button type="success" @click="clean_data">清除所有数据</el-button> <!-- <el-button type="danger" @click="onTest">测试</el-button> --> </el-form-item> <!-- Prompt到文案 --> <el-form-item label="Prompt"> <el-input v-model="form.chatgpt_prompt" :autosize="true" type="textarea" /> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmitGpt" >生成文案({{ wen_an_llm.name }})</el-button > <el-button type="danger" @click="marketingTemplateVisible = true" >通过营销模板生成文案</el-button > </el-form-item> <el-form-item label="文案"> <template v-if="marketing_template.result2"> <el-text class="mx-1" type="danger">第一次提问:</el-text> <el-text class="mx-1">{{ marketing_template.prompt1 }}</el-text> <el-text class="mx-1" type="danger">返回:</el-text> <el-text class="mx-1">{{ marketing_template.result1 }}</el-text> <el-text class="mx-1" type="danger">第二次提问:</el-text> <el-text class="mx-1">{{ marketing_template.prompt2 }}</el-text> </template> <el-input v-model="form.chatgpt_answer" :autosize="true" type="textarea" /> </el-form-item> <!-- 角色 --> <el-form-item> <el-button type="primary" @click="onAdaptRoles" >推理角色({{ role_llm.name }})</el-button > <el-button type="primary" @click="onAdaptRolesKeywords" >推理角色关键词({{ role_keywords_llm.name }})</el-button > <el-button plain @click="clean_roles">清空总角色列表</el-button> </el-form-item> <el-form-item label="角色"> <el-table :data="form.chatgpt_answer_roles" border style="width: 100%; z-index: calc(var(--el-table-index) -1)" > <el-table-column prop="角色" label="角色" width="300"> <template v-slot="scope"> <el-input v-model="scope.row.角色" :autosize="true" type="textarea" ></el-input> </template> </el-table-column> <el-table-column prop="属性" label="属性" width="300"> <template v-slot="scope"> <el-select v-model="scope.row.属性" filterable allow-create :reserve-keyword="false" > <el-option v-for="item in default_data.role_attribute_options" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template> </el-table-column> <el-table-column prop="角色关键词" label="角色关键词"> <template v-slot="scope"> <el-input v-model="scope.row.角色关键词" :autosize="true" type="textarea" ></el-input> </template> </el-table-column> <el-table-column prop="角色关键词英文" label="角色关键词英文"> <template v-slot="scope"> <el-input v-model="scope.row.角色关键词英文" :autosize="true" type="textarea" ></el-input> </template> </el-table-column> </el-table> </el-form-item> <!-- 分镜 --> <el-form-item> <el-button type="primary" @click="onAdapt('default')">分镜:标准模式</el-button> <el-button type="primary" @click="onAdapt('more_scene')" >分镜:更多场景模式</el-button > <el-button type="primary" @click="onAdaptScene" >推理场景({{ tuili_llm.name }})</el-button > <el-button type="primary" @click="onAdaptSceneRoles" >推理场景中的角色({{ tuili_keyword_llm.name }})</el-button > <el-button type="primary" @click="onDraw">绘图</el-button> <el-button plain @click="clean_scenes">清空分镜列表</el-button> </el-form-item> <el-form-item label="分镜"> <el-table :data="form.adapt_result_json" border style="width: 100%; z-index: calc(var(--el-table-index) -1)" > <el-table-column prop="编号" label="编号" width="55" /> <el-table-column prop="场景描述" label="场景描述"> <template v-slot="scope"> <el-input v-model="scope.row.场景描述" :autosize="true" type="textarea" ></el-input> </template> </el-table-column> <el-table-column prop="场景关键词" label="场景关键词"> <template v-slot="scope"> <el-text class="mx-1" size="small">{{ scope.row.场景关键词 }}</el-text> <hr style="border: none; border-top: 1px dashed #999; margin: 5px 0" /> <el-input v-model="scope.row.场景关键词英文" :autosize="true" type="textarea" ></el-input> </template> </el-table-column> <el-table-column prop="角色" label="角色"> <template v-slot="scope"> <el-text class="mx-1" size="small">{{ scope.row.info }}</el-text> <hr style="border: none; border-top: 1px dashed #999; margin: 5px 0" /> <el-select v-model="scope.row.角色" placeholder="" @change="onItemRolesChange(scope.row)" > <el-option v-for="item in scope.row.roles" :key="item" :label="item" :value="item" > <span style="float: left">{{ item }}</span> </el-option> </el-select> <!-- <el-text class="mx-1" size="small">{{ scope.row.角色 }}<br />({{ scope.row.info }})</el-text> --> </template> </el-table-column> <el-table-column prop="角色关键词" label="角色关键词"> <template v-slot="scope"> <el-text class="mx-1" size="small">{{ scope.row.角色关键词 }}</el-text> <hr style="border: none; border-top: 1px dashed #999; margin: 5px 0" /> <el-input v-model="scope.row.角色关键词英文" :autosize="true" type="textarea" ></el-input> </template> </el-table-column> <el-table-column prop="本镜配图" label="本镜配图" width="300"> <template v-slot="scope"> <div> <el-image :src="scope.row.本镜配图" :zoom-rate="1.0" :max-scale="1.5" :min-scale="1.5" :preview-src-list="[scope.row.本镜配图]" fit="cover" :hide-on-click-modal="true" /> </div> </template> </el-table-column> <el-table-column width="120" label="操作" align="center"> <!-- <template v-slot:header> <el-button type="danger" size="default" @click="">批量绘制所有图片</el-button> </template> --> <template v-slot="scope"> <div style="margin: 5px 0"> <el-button type="primary" size="small" @click="onAdaptOneScene(scope.row)" >推理场景</el-button > </div> <div style="margin: 5px 0"> <el-button type="primary" size="small" @click="onAdaptOneSceneRoles(scope.row)" >推理角色</el-button > </div> <div style="margin: 5px 0"> <el-button type="primary" size="small" @click="onDrawOne(scope.row)" >绘图</el-button > </div> <el-upload ref="upload" :show-file-list="false" :limit="1" accept=".png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.bmp,.BMP" :action="actionUrl" :on-success="handleUploadSuccess" :on-exceed="handleUploadExceed" :on-error="handleUploadError" :data="{ width: form.img_size.width, height: form.img_size.height }" :before-upload="handleBeforeUpload" > <el-button type="primary" size="small" @click="onClickUpload(scope.row)" >上传图片</el-button > </el-upload> <div style="margin: 5px 0"> <el-button type="primary" size="small" @click="showInPaintDialog('scene', scope.row)" >局部重绘</el-button > </div> <!-- <div style="margin: 5px 0"><el-button plain size="small" @click="onClearOnePic(scope.row)">清除图片</el-button></div> --> <div style="margin: 5px 0"> <el-button plain size="small" @click="showsdprompt(scope.row)" >debug</el-button > </div> <el-dialog v-model="dialogVisible" width="80%"> <p>{{ dialogData }}</p> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="dialogVisible = false" >ok</el-button > </div> </template> </el-dialog> <div style="margin: 10px 0"> <el-button type="danger" size="small" @click="onDeleteOne(scope.row)" >删除本镜</el-button > </div> </template> </el-table-column> </el-table> </el-form-item> <el-form-item label="封面图片"> <el-switch v-model="cover_backcover.if_need_cover_pic" active-value="true" inactive-value="false" /> </el-form-item> <div v-if="JSON.parse(cover_backcover.if_need_cover_pic.toLowerCase())"> <el-form-item> <!-- :style="{ width: String(parseInt(form.img_size.width) / 3)+'px', height: String(parseInt(form.img_size.height) / 3)+'px' }" --> <div :style="{ width: String(parseInt(form.img_size.width) / 3) + 'px' }" class="dashed-div" > <el-image :src="cover_backcover.cover_pic" /> </div> <div :style="{ width: String(parseInt(form.img_size.width) / 3) + 'px' }" class="dashed-div" style="margin-left: 20px" > <el-image :src="cover_backcover.cover_pic_with_text" /> </div> </el-form-item> <el-form-item> <el-upload ref="upload_cover" :show-file-list="false" :limit="1" accept=".png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.bmp,.BMP" :action="actionUrl" :on-success="MarketingTemplateUploadCoverPicSuccess" :on-exceed="handleMarketingTemplateUploadCoverPicExceed" :on-error="handleUploadError" :data="{ width: form.img_size.width, height: form.img_size.height }" :before-upload="handleBeforeUpload" > <el-button type="primary" size="small">上传图片</el-button> </el-upload> <span style="margin-left: 12px">或使用第</span> <el-select size="small" v-model="cover_backcover.cover_pic_use_scene" placeholder="选择" style="width: 60px" @change="onSelectCoverChange" > <el-option v-for="item in form.adapt_result_json" :key="item.编号" :label="item.编号" :value="item.编号" > <span style="float: left">{{ item.编号 }}</span> </el-option> </el-select> <span>帧做封面</span> <el-button type="danger" size="small" @click="onClearMarketingTemplatePic('cover')" style="margin-left: 12px" >清除图片</el-button > <el-button type="primary" size="small" @click="showInPaintDialog('cover', '')" style="margin-left: 12px" >局部重绘</el-button > </el-form-item> <el-form-item> <div style="width: 100%"> <el-button type="primary" size="small" @click="onAddMarketingTemplatePicText('cover', 'add', 0)" >增加文字</el-button > <!-- <el-button type="success" size="small" @click="onMarketingTemplateAddTextToPic('cover')">预览</el-button> --> </div> <div v-for="(pic_title, index) in cover_backcover.cover_pic_titles" :key="index" class="dashed-div" style="width: 100%" > <el-input v-model="pic_title.text" :autosize="true" type="textarea" @change="onMarketingTemplateAddTextToPic('cover')" ></el-input> <span style="margin-left: 10px">字体颜色:</span> <el-color-picker v-model="pic_title.color" @change="onMarketingTemplateAddTextToPic('cover')" /> <span style="margin-left: 30px">字体背景:</span> <el-color-picker v-model="pic_title.bg_color" @change="onMarketingTemplateAddTextToPic('cover')" /> <span style="margin-left: 30px">字体大小:</span> <el-input-number v-model="pic_title.font_size" :min="1" :max="100" controls-position="right" @change="onMarketingTemplateAddTextToPic('cover')" /> <span style="margin-left: 30px">在图片上的位置:</span> <el-slider v-model="pic_title.position" :step="0.01" :min="0" :max="1" show-input vertical height="100px" style="margin-top: 10px" @change="onMarketingTemplateAddTextToPic('cover')" /> <el-button type="danger" size="small" @click="onAddMarketingTemplatePicText('cover', 'del', index)" style="margin-left: 80px" >删除文字</el-button > </div> </el-form-item> </div> <el-form-item label="封底图片"> <el-switch v-model="cover_backcover.if_need_product_pic" active-value="true" inactive-value="false" /> </el-form-item> <div v-if="JSON.parse(cover_backcover.if_need_product_pic.toLowerCase())"> <el-form-item> <!-- :style="{ width: String(parseInt(form.img_size.width) / 3)+'px', height: String(parseInt(form.img_size.height) / 3)+'px' }" --> <div :style="{ width: String(parseInt(form.img_size.width) / 3) + 'px' }" class="dashed-div" > <el-image :src="cover_backcover.product_pic" /> </div> <div :style="{ width: String(parseInt(form.img_size.width) / 3) + 'px' }" class="dashed-div" style="margin-left: 20px" > <el-image :src="cover_backcover.product_pic_with_text" /> </div> </el-form-item> <el-form-item> <el-upload ref="upload_product" :show-file-list="false" :limit="1" accept=".png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.bmp,.BMP" :action="actionUrl" :on-success="MarketingTemplateUploadProductPicSuccess" :on-exceed="handleMarketingTemplateUploadProductPicExceed" :on-error="handleUploadError" :data="{ width: form.img_size.width, height: form.img_size.height }" :before-upload="handleBeforeUpload" > <el-button type="primary" size="small">上传图片</el-button> </el-upload> <el-button type="danger" size="small" @click="onClearMarketingTemplatePic('product')" style="margin-left: 12px" >清除图片</el-button > <el-button type="primary" size="small" @click="showInPaintDialog('product', '')" style="margin-left: 12px" >局部重绘</el-button > </el-form-item> <el-form-item> <div style="width: 100%"> <el-button type="primary" size="small" @click="onAddMarketingTemplatePicText('product', 'add', 0)" >增加文字</el-button > <!-- <el-button type="success" size="small" @click="onMarketingTemplateAddTextToPic('product')">预览</el-button> --> </div> <div v-for="(pic_title, index) in cover_backcover.product_pic_titles" :key="index" class="dashed-div" style="width: 100%" > <el-input v-model="pic_title.text" :autosize="true" type="textarea" @change="onMarketingTemplateAddTextToPic('product')" ></el-input> <span style="margin-left: 10px">字体颜色:</span> <el-color-picker v-model="pic_title.color" @change="onMarketingTemplateAddTextToPic('product')" /> <span style="margin-left: 30px">字体背景:</span> <el-color-picker v-model="pic_title.bg_color" @change="onMarketingTemplateAddTextToPic('product')" /> <span style="margin-left: 30px">字体大小:</span> <el-input-number v-model="pic_title.font_size" :min="1" :max="100" controls-position="right" @change="onMarketingTemplateAddTextToPic('product')" /> <span style="margin-left: 30px">在图片上的位置:</span> <el-slider v-model="pic_title.position" :step="0.01" :min="0" :max="1" show-input vertical height="100px" style="margin-top: 10px" @change="onMarketingTemplateAddTextToPic('product')" /> <el-button type="danger" size="small" @click="onAddMarketingTemplatePicText('product', 'del', index)" style="margin-left: 80px" >删除文字</el-button > </div> </el-form-item> <el-form-item label="封底旁白"> <el-input v-model="cover_backcover.product_pic_speech" :autosize="true" type="textarea" ></el-input> </el-form-item> </div> <!-- 生成视频 --> <el-form-item label="视频设置"> <span style="margin: 0 20px 0 0">TTS语速:</span> <el-slider v-model="voice_rate" show-input :min="-50" :max="50" :marks="default_data.marks" style="width: 900px" /> </el-form-item> <el-form-item> <span style="margin: 0 20px 0 0">TTS音量:</span> <el-slider v-model="voice_volume" show-input :min="-80" :max="80" :marks="default_data.marks" style="width: 900px" /> </el-form-item> <el-form-item> <span style="margin: 20px 20px 0 0">TTS语音:</span> <el-select v-model="voice" placeholder="Select" style="width: 400px; margin-top: 20px" > <el-option v-for="item in default_data.voices" :key="item.value" :label="item.value" :value="item.value" > <span style="float: left">{{ item.value }}</span> <span style="float: right; color: var(--el-text-color-secondary); font-size: 13px" >{{ item.label }}</span > </el-option> </el-select> <audio :src="'src/assets/edge-tts-voices/' + voice + '.mp3'" controls style="height: 30px; margin: 20px 0 0 10px" ></audio> </el-form-item> <el-form-item> <span style="margin: 0 20px 0 0">背景音乐:</span> <el-select v-model="bgm" placeholder="无" style="width: 400px"> <el-option v-for="item in default_data.bgm" :key="item.value" :label="item.value" :value="item.value" > <span style="float: left">{{ item.label }}</span> <span style="float: right; color: var(--el-text-color-secondary); font-size: 13px" >{{ item.value }}</span > </el-option> </el-select> <audio :src="'src/assets/bgm/' + bgm + '.mp3'" controls style="height: 30px; margin-left: 10px" ></audio> </el-form-item> <el-form-item> <span style="margin: 0 20px 0 0">背景音量:</span> <el-slider v-model="bgm_volume" show-input :step="0.1" :min="0" :max="2" :marks="default_data.bgm_volume_marks" style="width: 600px" /> </el-form-item> <el-form-item> <span style="margin: 20px 20px 20px 0">字幕合成:</span> <el-switch v-model="form.if_need_subtitle" active-value="true" inactive-value="false" /> <div v-if="JSON.parse(form.if_need_subtitle.toLowerCase())"> <span style="margin-left: 30px">字体颜色:</span> <el-color-picker v-model="sub_font_color" /> <span style="margin-left: 30px">字体背景:</span> <el-color-picker v-model="sub_bg_color" /> <span style="margin-left: 30px">字体大小:</span> <el-input-number v-model="sub_font_size" :min="1" :max="50" controls-position="right" /> <span style="margin-left: 30px">在屏幕上的位置:</span> <el-slider v-model="sub_position" :step="0.1" :min="0" :max="1" show-input vertical height="100px" /> </div> </el-form-item> <el-form-item> <el-button type="primary" @click="onGenVideo">生成视频</el-button> </el-form-item> <el-form-item> <video :src="form.final_video" controls></video> </el-form-item> </el-form> <!-- 授权密码框 --> <el-dialog v-model="pwdCheckDialogVisible" title="请输入密码" width="20%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" > <el-form :model="form"> <el-form-item label="密码"> <el-input v-model="pwdCheckValue" autocomplete="off" type="password" show-password @keyup.enter="onPwdCheckDialog()" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="onPwdCheckDialog()">ok</el-button> </div> </template> </el-dialog> <!-- 营销模板 --> <el-dialog v-model="marketingTemplateVisible" title="营销模板" width="60%" height="500" :close-on-click-modal="false" :close-on-press-escape="true" :show-close="true" :lock-scroll="true" > <!-- <el-tabs v-model="activeTab"> <el-tab-pane label="商品" name="first">User</el-tab-pane> <el-tab-pane label="Config" name="second">Config</el-tab-pane> <el-tab-pane label="Role" name="third">Role</el-tab-pane> <el-tab-pane label="Task" name="fourth">Task</el-tab-pane> </el-tabs> <el-button @click="goToNextPage">下一步</el-button> --> <el-form :model="marketing_template"> <el-form-item label="商品名称"> <el-input v-model="marketing_template.product_name" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="商品描述"> <el-input v-model="marketing_template.product_description" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="目标群体"> <el-input v-model="marketing_template.target_people" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="文案主角"> <el-input v-model="marketing_template.text_role" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="文案风格"> <el-input v-model="marketing_template.text_style" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="故事类型"> <el-input v-model="marketing_template.story_type" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="参考信息"> <el-input v-model="marketing_template.reference" :autosize="true" type="textarea" ></el-input> </el-form-item> <el-form-item label="文案字数"> <el-input-number v-model="marketing_template.words_num" :min="100" :max="500" controls-position="right" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="marketingTemplateVisible = false">取消</el-button> <el-button type="primary" @click="onMarketingTemplateSubmitGpt()" >提交</el-button > </div> </template> </el-dialog> <!-- 局部重绘 --> <el-dialog v-model="inPaintVisible" :width="parseInt(form.img_size.width) + 40" :close-on-click-modal="false" :close-on-press-escape="true" :lock-scroll="true" > <div style="color: red">请在图片上涂抹需要保留的部分</div> <div ref="inpaint" :style=" 'position: relative; width: ' + form.img_size.width + 'px; height: ' + form.img_size.height + 'px; margin: 10px auto;' " > <canvas id="baseCanvas" :width="form.img_size.width" :height="form.img_size.height" style="position: absolute; left: 0; top: 0" ></canvas> <canvas id="maskCanvas" :width="form.img_size.width" :height="form.img_size.height" style="position: absolute; left: 0; top: 0; z-index: 1; background: none" ></canvas> </div> <button id="clearButton">清除涂抹</button> <div> <span>画面描述:</span> <el-input v-model="inPaintPrompt" :autosize="true" type="textarea"></el-input> </div> <template #footer> <div class="dialog-footer"> <el-button @click="inPaintVisible = false">取消</el-button> <el-button type="primary" @click="onSubmitInPaint">提交</el-button> </div> </template> </el-dialog> </main> </template> <style lang="scss" scoped> .home-container { width: 100%; } </style> <style lang="scss"> .home-container { .el-table .el-table__cell { z-index: calc(var(--el-table-index) -1); } } .dashed-div { border: 1px dashed #999; margin: 5px 0; padding: 2px; } </style>