python+vue编写一个ai生成电影短剧故事片视频代码
最近ai在不断的向视频影视角度发展,国内的阿里通义wanx2.1开源了,普通的电脑就能部署了,这也加快了ai电影影视创作的步伐,今天我们以阿里的通义万相文生视频功能来制作一个ai一键成片,自动创作故事视频短剧短视频等内容,只要你的算力允许,用他只一个ai电影也不再话下,先看看步骤:
前端代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI 短片生成器</title> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/vue@2.6.1-dev.js"></script> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/axios.1.4.0.js"></script> <style> :root { --primary-color: #4a6bfd; --secondary-color: #6f42c1; --accent-color: #fd7e14; --success-color: #28a745; --danger-color: #dc3545; --light-color: #f8f9fa; --dark-color: #343a40; --text-color: #212529; --border-radius: 8px; --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --transition-speed: 0.3s; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: var(--text-color); background-color: #f5f7fa; margin: 0; padding: 0; } #app { max-width: 1200px; margin: 0 auto; padding: 20px; } .container { background-color: #fff; border-radius: var(--border-radius); box-shadow: var(--box-shadow); padding: 30px; margin-bottom: 30px; } h2 { color: var(--primary-color); margin-bottom: 20px; font-weight: 600; position: relative; padding-bottom: 10px; font-size: 1.8rem; } h2:after { content: ''; position: absolute; bottom: 0; left: 0; width: 60px; height: 3px; background: var(--primary-color); border-radius: 3px; } h3 { color: var(--secondary-color); margin: 15px 0; font-weight: 500; font-size: 1.2rem; } .step { display: none; animation: fadeIn 0.5s ease-in-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .step.active { display: block; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; color: var(--dark-color); } textarea, input[type="text"] { width: 100%; padding: 12px 15px; margin-bottom: 15px; border: 1px solid #ced4da; border-radius: var(--border-radius); font-size: 16px; transition: border-color var(--transition-speed); background-color: #fcfdff; color: var(--text-color); } textarea:focus, input:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(74, 107, 253, 0.25); } textarea { min-height: 150px; resize: vertical; } select { display: block; width: 100%; padding: 12px 15px; margin-bottom: 15px; border: 1px solid #ced4da; border-radius: var(--border-radius); font-size: 16px; background-color: #fcfdff; background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 15px center; background-size: 8px 10px; -webkit-appearance: none; -moz-appearance: none; appearance: none; } select:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(74, 107, 253, 0.25); } .btn-container { display: flex; justify-content: space-between; margin-top: 20px; } button { padding: 12px 25px; border: none; border-radius: var(--border-radius); font-size: 16px; font-weight: 500; cursor: pointer; transition: all var(--transition-speed); display: inline-flex; align-items: center; justify-content: center; } button:focus { outline: none; } .btn-primary { background-color: var(--primary-color); color: white; } .btn-primary:hover { background-color: #3a5cfd; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(74, 107, 253, 0.3); } .btn-primary:disabled { background-color: #a5b3f5; cursor: not-allowed; transform: none; box-shadow: none; } .btn-secondary { background-color: #e9ecef; color: var(--dark-color); } .btn-secondary:hover { background-color: #dde2e6; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .btn-danger { background-color: var(--danger-color); color: white; } .btn-danger:hover { background-color: #c82333; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3); } .btn-success { background-color: var(--success-color); color: white; } .btn-success:hover { background-color: #218838; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3); } .btn-icon { margin-right: 8px; } .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 9999; backdrop-filter: blur(5px); } .loading-spinner { width: 70px; height: 70px; border: 5px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: var(--primary-color); animation: spin 1s ease-in-out infinite; margin-bottom: 20px; } .loading-text { color: white; font-size: 18px; font-weight: 500; } @keyframes spin { to { transform: rotate(360deg); } } .shot-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-top: 20px; margin-bottom: 20px; } .shot-item { background-color: white; border-radius: var(--border-radius); box-shadow: var(--box-shadow); padding: 20px; transition: transform var(--transition-speed), box-shadow var(--transition-speed); } .shot-item:hover { transform: translateY(-5px); box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); } .shot-item img, .shot-item video { width: 100%; border-radius: var(--border-radius); margin-bottom: 15px; object-fit: cover; height: 200px; background-color: #f8f9fa; } .shot-controls { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; } .shot-controls button { flex: 1; min-width: 100px; padding: 8px 15px; font-size: 14px; } .section-title { display: flex; align-items: center; margin-bottom: 20px; } .section-icon { margin-right: 10px; font-size: 24px; color: var(--primary-color); } .voice-selection, .music-selection, .transition-selection { background-color: white; border-radius: var(--border-radius); padding: 20px; margin-bottom: 20px; box-shadow: var(--box-shadow); } .file-upload { margin-top: 15px; } .file-upload label { display: block; background-color: #e9ecef; color: var(--dark-color); padding: 12px 15px; border-radius: var(--border-radius); cursor: pointer; text-align: center; transition: background-color var(--transition-speed); } .file-upload label:hover { background-color: #dde2e6; } .file-upload input[type="file"] { display: none; } .file-upload .file-name { margin-top: 10px; font-size: 14px; color: #6c757d; } .video-preview { background-color: white; border-radius: var(--border-radius); padding: 20px; margin: 20px 0; box-shadow: var(--box-shadow); } .video-preview video { width: 100%; max-width: 100%; border-radius: var(--border-radius); background-color: #f8f9fa; } .step-indicator { display: flex; justify-content: space-between; margin-bottom: 30px; position: relative; } .step-indicator::before { content: ''; position: absolute; top: 20px; left: 0; right: 0; height: 2px; background-color: #e9ecef; z-index: 1; } .step-item { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 2; } .step-number { width: 40px; height: 40px; border-radius: 50%; background-color: #e9ecef; color: var(--dark-color); display: flex; align-items: center; justify-content: center; font-weight: 500; margin-bottom: 10px; transition: all var(--transition-speed); } .step-text { font-size: 14px; font-weight: 500; color: #6c757d; transition: color var(--transition-speed); } .step-item.active .step-number { background-color: var(--primary-color); color: white; } .step-item.active .step-text { color: var(--primary-color); } .step-item.completed .step-number { background-color: var(--success-color); color: white; } @media (max-width: 768px) { .shot-container { grid-template-columns: 1fr; } .btn-container { flex-direction: column; gap: 10px; } button { width: 100%; } .step-text { font-size: 12px; } } </style> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> </head> <body> <div id="app"> <!-- 全屏遮罩 --> <div class="loading-overlay" v-if="loading"> <div class="loading-spinner"></div> <div class="loading-text">{{ loadingMessage }}</div> </div> <!-- 步骤指示器 --> <div class="step-indicator"> <div class="step-item" :class="{ 'active': step === 1, 'completed': step > 1 }"> <div class="step-number">1</div> <div class="step-text">输入主题</div> </div> <div class="step-item" :class="{ 'active': step === 2, 'completed': step > 2 }"> <div class="step-number">2</div> <div class="step-text">生成剧本</div> </div> <div class="step-item" :class="{ 'active': step === 3, 'completed': step > 3 }"> <div class="step-number">3</div> <div class="step-text">分镜脚本</div> </div> <div class="step-item" :class="{ 'active': step === 4, 'completed': step > 4 }"> <div class="step-number">4</div> <div class="step-text">配音与音效</div> </div> <div class="step-item" :class="{ 'active': step === 5 }"> <div class="step-number">5</div> <div class="step-text">完成视频</div> </div> </div> <!-- 步骤 1:输入主题 --> <div class="step container" :class="{ active: step === 1 }"> <div class="section-title"> <i class="fas fa-lightbulb section-icon"></i> <h2>输入主题</h2> </div> <div class="form-group"> <label for="topic">视频主题</label> <input id="topic" type="text" v-model="topic" placeholder="请输入视频主题,如:'人工智能的未来'" /> </div> <div class="form-group"> <label for="aspectratio">视频尺寸</label> <select id="aspectratio" v-model="aspectratio"> <option value="" disabled selected>请选择尺寸</option> <option value="720*1280">9:16 手机竖屏</option> <option value="1280*720">16:9 宽屏</option> <option value="960*960">1:1 方形</option> </select> </div> <div class="form-group"> <label for="fenge">视频风格</label> <select id="fenge" v-model="fenge"> <option value="" disabled selected>请选择风格</option> <option value="漫画推文">漫画风格</option> <option value="儿童绘本 ">儿童绘本 </option> <option value="玄幻修真 ">玄幻修真 </option> <option value="民间故事 "> 民间故事 </option> <option value="悬疑推理 ">悬疑推理</option> <option value="都市剧情 ">都市剧情</option> <option value="恐怖片">恐怖片</option> </select> </div> <div class="btn-container"> <div></div> <!-- 占位 --> <button class="btn-primary" @click="nextStep" :disabled="!topic || !aspectratio"> <i class="fas fa-arrow-right btn-icon"></i>下一步 </button> </div> </div> <!-- 步骤 2:生成剧本 --> <div class="step container" :class="{ active: step === 2 }"> <div class="section-title"> <i class="fas fa-book-open section-icon"></i> <h2>生成剧本</h2> </div> <button class="btn-primary" @click="generateStory" :disabled="loadingStory"> <i class="fas fa-magic btn-icon"></i>生成剧本 </button> <div class="form-group" style="margin-top: 20px;"> <textarea v-model="story" rows="10" placeholder="生成的剧本将显示在这里,您也可以自行编辑"></textarea> </div> <div class="btn-container"> <button class="btn-secondary" @click="prevStep"> <i class="fas fa-arrow-left btn-icon"></i>上一步 </button> <button class="btn-primary" @click="nextStep" :disabled="!story"> <i class="fas fa-arrow-right btn-icon"></i>下一步 </button> </div> </div> <!-- 步骤 3:生成分镜头脚本 --> <div class="step container" :class="{ active: step === 3 }"> <div class="section-title"> <i class="fas fa-film section-icon"></i> <h2>分镜头脚本</h2> </div> <button class="btn-primary" @click="generateShots" :disabled="loadingShots"> <i class="fas fa-magic btn-icon"></i>生成分镜头脚本 </button> <div class="shot-container"> <div v-for="(shot, index) in shots" :key="index" class="shot-item"> <div v-if="!shot.isvideo && shot.imageUrl" class="shot-preview"> <img :src="shot.imageUrl" alt="分镜头图片" /> </div> <div v-else-if="shot.isvideo && shot.videoUrl" class="shot-preview"> <video controls :src="shot.videoUrl"></video> </div> <div v-else class="shot-preview" style="height: 200px; background-color: #f8f9fa; display: flex; justify-content: center; align-items: center;"> <i class="fas fa-image" style="font-size: 48px; color: #ced4da;"></i> </div> <div class="form-group"> <input type="text" v-model="shot.prompt" placeholder="分镜头提示词(描述这个画面的内容)" /> </div> <div class="form-group"> <input type="text" v-model="shot.sayword" placeholder="文字解说(这个画面的配音内容)" /> </div> <div class="shot-controls"> <button class="btn-primary" @click="generateImage(index)"> <i class="fas fa-image btn-icon"></i>生成图片 </button> <button class="btn-success" @click="generateVideo(index)"> <i class="fas fa-video btn-icon"></i>生成视频 </button> <button class="btn-danger" @click="removeShot(index)"> <i class="fas fa-trash-alt btn-icon"></i>删除 </button> </div> </div> </div> <button class="btn-secondary" style="margin-top: 20px; width: 100%;" @click="addShot"> <i class="fas fa-plus btn-icon"></i>添加分镜头 </button> <div class="btn-container" style="margin-top: 30px;"> <button class="btn-secondary" @click="prevStep"> <i class="fas fa-arrow-left btn-icon"></i>上一步 </button> <button class="btn-primary" @click="nextStep" :disabled="shots.length === 0"> <i class="fas fa-arrow-right btn-icon"></i>下一步 </button> </div> </div> <!-- 步骤 4:选择配音和背景音乐 --> <div class="step container" :class="{ active: step === 4 }"> <div class="section-title"> <i class="fas fa-microphone-alt section-icon"></i> <h2>配音与音效</h2> </div> <!-- 配音音色选择 --> <div class="voice-selection"> <h3><i class="fas fa-user-voice"></i> 配音音色</h3> <select v-model="selectedVoice"> <option value="" disabled selected>请选择配音音色</option> <optgroup v-for="language in groupedVoices" :label="language.name"> <option v-for="voice in language.voices" :value="voice.id"> {{ voice.name }} - {{ voice.feature }} ({{ voice.scene }}) </option> </optgroup> </select> </div> <!-- 背景音乐选择或上传 --> <div class="music-selection"> <h3><i class="fas fa-music"></i> 背景音乐</h3> <select v-model="selectedBackgroundMusic"> <option value="">无背景音乐</option> <option v-for="music in backgroundMusicList" :value="music.id">{{ music.name }}</option> </select> <div class="file-upload"> <label for="music-upload"> <i class="fas fa-upload"></i> 上传自定义背景音乐 </label> <input id="music-upload" type="file" @change="handleBackgroundMusicUpload" accept="audio/*" /> <div class="file-name" v-if="uploadedBackgroundMusic"> 已选择: {{ uploadedBackgroundMusic.name }} </div> </div> </div> <!-- 过渡效果选择 --> <div class="transition-selection"> <h3><i class="fas fa-random"></i> 画面过渡效果</h3> <select v-model="selectedTransition"> <option value="none">无过渡效果</option> <option value="fade">淡入淡出</option> <option value="slide">滑动</option> <option value="zoom">缩放</option> <option value="wipe">擦除</option> <option value="crossfade">交叉淡入淡出</option> </select> </div> <div class="btn-container"> <button class="btn-secondary" @click="prevStep"> <i class="fas fa-arrow-left btn-icon"></i>上一步 </button> <button class="btn-primary" @click="combineVideos" :disabled="!selectedVoice"> <i class="fas fa-magic btn-icon"></i>合成视频 </button> </div> </div> <!-- 步骤 5:合成视频 --> <div class="step container" :class="{ active: step === 5 }"> <div class="section-title"> <i class="fas fa-check-circle section-icon"></i> <h2>视频制作完成</h2> </div> <div class="video-preview"> <h3><i class="fas fa-film"></i> 成品视频</h3> <video :src="finalVideoUrl" controls></video> </div> <div class="btn-container"> <button class="btn-secondary" @click="prevStep"> <i class="fas fa-arrow-left btn-icon"></i>返回编辑 </button> <button class="btn-success" @click="downloadVideo"> <i class="fas fa-download btn-icon"></i>下载视频 </button> </div> </div> </div> <script> new Vue({ el: '#app', data: { step: 1, topic: '', projectid: "", story: '', shots: [], videos: [], fenge: "", aspectratio: "", finalVideoUrl: '', loadingStory: false, loadingShots: false, loadingVideos: false, loading: false, loadingMessage: "处理中,请稍候...", selectedVoice: '', selectedBackgroundMusic: '', selectedTransition: 'fade', voices: [ { id: 'sambert-zhinan-v1', name: '知楠', model: 'sambert-zhinan-v1', timestampSupport: true, scene: '通用场景', feature: '广告男声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhiqi-v1', name: '知琪', model: 'sambert-zhiqi-v1', timestampSupport: true, scene: '通用场景', feature: '温柔女声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhichu-v1', name: '知厨', model: 'sambert-zhichu-v1', timestampSupport: true, scene: '新闻播报', feature: '舌尖男声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhide-v1', name: '知德', model: 'sambert-zhide-v1', timestampSupport: true, scene: '新闻播报', feature: '新闻男声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhijia-v1', name: '知佳', model: 'sambert-zhijia-v1', timestampSupport: true, scene: '新闻播报', feature: '标准女声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhiru-v1', name: '知茹', model: 'sambert-zhiru-v1', timestampSupport: true, scene: '新闻播报', feature: '新闻女声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhiqian-v1', name: '知倩', model: 'sambert-zhiqian-v1', timestampSupport: true, scene: '配音解说、新闻播报', feature: '资讯女声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhixiang-v1', name: '知祥', model: 'sambert-zhixiang-v1', timestampSupport: true, scene: '配音解说', feature: '磁性男声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhiwei-v1', name: '知薇', model: 'sambert-zhiwei-v1', timestampSupport: true, scene: '阅读产品简介', feature: '萝莉女声', language: '中文+英文', sampleRate: '48k' }, { id: 'sambert-zhihao-v1', name: '知浩', model: 'sambert-zhihao-v1', timestampSupport: true, scene: '通用场景', feature: '咨询男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhijing-v1', name: '知婧', model: 'sambert-zhijing-v1', timestampSupport: true, scene: '通用场景', feature: '严厉女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiming-v1', name: '知茗', model: 'sambert-zhiming-v1', timestampSupport: true, scene: '通用场景', feature: '诙谐男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhimo-v1', name: '知墨', model: 'sambert-zhimo-v1', timestampSupport: true, scene: '通用场景', feature: '情感男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhina-v1', name: '知娜', model: 'sambert-zhina-v1', timestampSupport: true, scene: '通用场景', feature: '浙普女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhishu-v1', name: '知树', model: 'sambert-zhishu-v1', timestampSupport: true, scene: '通用场景', feature: '资讯男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhistella-v1', name: '知莎', model: 'sambert-zhistella-v1', timestampSupport: true, scene: '通用场景', feature: '知性女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiting-v1', name: '知婷', model: 'sambert-zhiting-v1', timestampSupport: true, scene: '通用场景', feature: '电台女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhixiao-v1', name: '知笑', model: 'sambert-zhixiao-v1', timestampSupport: true, scene: '通用场景', feature: '资讯女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiya-v1', name: '知雅', model: 'sambert-zhiya-v1', timestampSupport: true, scene: '通用场景', feature: '严厉女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiye-v1', name: '知晔', model: 'sambert-zhiye-v1', timestampSupport: true, scene: '通用场景', feature: '青年男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiying-v1', name: '知颖', model: 'sambert-zhiying-v1', timestampSupport: true, scene: '通用场景', feature: '软萌童声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiyuan-v1', name: '知媛', model: 'sambert-zhiyuan-v1', timestampSupport: true, scene: '通用场景', feature: '知心姐姐', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhiyue-v1', name: '知悦', model: 'sambert-zhiyue-v1', timestampSupport: true, scene: '客服', feature: '温柔女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhigui-v1', name: '知柜', model: 'sambert-zhigui-v1', timestampSupport: true, scene: '阅读产品简介', feature: '直播女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhishuo-v1', name: '知硕', model: 'sambert-zhishuo-v1', timestampSupport: true, scene: '数字人', feature: '自然男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhimiao-emo-v1', name: '知妙(多情感)', model: 'sambert-zhimiao-emo-v1', timestampSupport: true, scene: '阅读产品简介、数字人、直播', feature: '多种情感女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhimao-v1', name: '知猫', model: 'sambert-zhimao-v1', timestampSupport: true, scene: '阅读产品简介、配音解说、数字人、直播', feature: '直播女声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhilun-v1', name: '知伦', model: 'sambert-zhilun-v1', timestampSupport: true, scene: '配音解说', feature: '悬疑解说', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhifei-v1', name: '知飞', model: 'sambert-zhifei-v1', timestampSupport: true, scene: '配音解说', feature: '激昂解说', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-zhida-v1', name: '知达', model: 'sambert-zhida-v1', timestampSupport: true, scene: '新闻播报', feature: '标准男声', language: '中文+英文', sampleRate: '16k' }, { id: 'sambert-camila-v1', name: 'Camila', model: 'sambert-camila-v1', timestampSupport: false, scene: '通用场景', feature: '西班牙语女声', language: '西班牙语', sampleRate: '16k' }, { id: 'sambert-perla-v1', name: 'Perla', model: 'sambert-perla-v1', timestampSupport: false, scene: '通用场景', feature: '意大利语女声', language: '意大利语', sampleRate: '16k' }, { id: 'sambert-indah-v1', name: 'Indah', model: 'sambert-indah-v1', timestampSupport: false, scene: '通用场景', feature: '印尼语女声', language: '印尼语', sampleRate: '16k' }, { id: 'sambert-clara-v1', name: 'Clara', model: 'sambert-clara-v1', timestampSupport: false, scene: '通用场景', feature: '法语女声', language: '法语', sampleRate: '16k' }, { id: 'sambert-hanna-v1', name: 'Hanna', model: 'sambert-hanna-v1', timestampSupport: false, scene: '通用场景', feature: '德语女声', language: '德语', sampleRate: '16k' }, { id: 'sambert-beth-v1', name: 'Beth', model: 'sambert-beth-v1', timestampSupport: true, scene: '通用场景', feature: '咨询女声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-betty-v1', name: 'Betty', model: 'sambert-betty-v1', timestampSupport: true, scene: '通用场景', feature: '客服女声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-cally-v1', name: 'Cally', model: 'sambert-cally-v1', timestampSupport: true, scene: '通用场景', feature: '自然女声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-cindy-v1', name: 'Cindy', model: 'sambert-cindy-v1', timestampSupport: true, scene: '通用场景', feature: '对话女声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-eva-v1', name: 'Eva', model: 'sambert-eva-v1', timestampSupport: true, scene: '通用场景', feature: '陪伴女声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-donna-v1', name: 'Donna', model: 'sambert-donna-v1', timestampSupport: true, scene: '通用场景', feature: '教育女声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-brian-v1', name: 'Brian', model: 'sambert-brian-v1', timestampSupport: true, scene: '通用场景', feature: '客服男声', language: '美式英文', sampleRate: '16k' }, { id: 'sambert-waan-v1', name: 'Waan', model: 'sambert-waan-v1', timestampSupport: false, scene: '通用场景', feature: '泰语女声', language: '泰语', sampleRate: '16k' } ], backgroundMusicList: [ { id: '5c89fd22dea6948307.mp3', name: '轻松愉快 - 适合产品介绍' }, { id: '5c89fd22dea6948308.mp3', name: '激情澎湃 - 适合运动场景' }, { id: '5c89fd22dea6948309.mp3', name: '柔和舒缓 - 适合自然风景' }, { id: '5c89fd22dea6948310.mp3', name: '紧张刺激 - 适合悬疑场景' }, { id: '5c89fd22dea6948311.mp3', name: '温馨感人 - 适合情感故事' } ], uploadedBackgroundMusic: null }, computed: { groupedVoices() { const langs = {}; this.voices.forEach(voice => { if (!langs[voice.language]) { langs[voice.language] = { name: voice.language, voices: [] }; } langs[voice.language].voices.push(voice); }); return Object.values(langs); } }, mounted() { this.projectid = this.generateRandomString(8); }, methods: { generateRandomString(length) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; }, handleBackgroundMusicUpload(event) { const file = event.target.files[0]; if (file) { this.uploadedBackgroundMusic = file; this.selectedBackgroundMusic = 'uploaded'; // 标记为上传文件 } }, nextStep() { this.step++; window.scrollTo(0, 0); }, prevStep() { this.step--; window.scrollTo(0, 0); }, async generateStory() { this.loading = true; this.loadingMessage = "正在创作精彩剧本,请稍候..."; this.loadingStory = true; try { const response = await axios.post('/generate_story', { projectid: this.projectid, topic: this.topic }); this.story = response.data.story; } catch (error) { this.$nextTick(() => { alert('生成剧本失败,请重试'); }); } finally { this.loading = false; this.loadingStory = false; } }, async generateShots() { this.loading = true; this.loadingMessage = "正在创建分镜头脚本,这可能需要一点时间..."; this.loadingShots = true; var that = this; try { const response = await axios.post('/generate_shots', { projectid: this.projectid, story: this.story }); let newshots = response.data.shots || []; for (let i = 0; i < newshots.length; i++) { newshots[i].imageUrl = ''; newshots[i].videoUrl = ''; newshots[i].isvideo = false; } that.shots = newshots; } catch (error) { this.$nextTick(() => { alert('生成分镜头脚本失败,请重试'); }); } finally { this.loading = false; this.loadingShots = false; } }, addShot() { this.shots.push({ prompt: '', sayword: '', imageUrl: '', videoUrl: "", isvideo: false }); }, removeShot(index) { this.shots.splice(index, 1); }, async generateImage(index) { if (!this.shots[index].prompt) { alert('请先输入分镜头提示词'); return; } this.loading = true; this.loadingMessage = "正在生成图片,请稍候..."; try { const response = await axios.post('/generate_image', { fenge: this.fenge, projectid: this.projectid, prompt: this.shots[index].prompt, ratio: this.aspectratio }); let olditem = this.shots[index]; olditem.imageUrl = response.data.image_url; olditem.isvideo = false; this.$set(this.shots, index, olditem); } catch (error) { this.$nextTick(() => { alert('生成图片失败,请重试'); }); } finally { this.loading = false; } }, async generateVideo(index) { if (!this.shots[index].prompt) { alert('请先输入分镜头提示词'); return; } this.loading = true; this.loadingMessage = "正在生成视频片段,这需要一些时间..."; try { const response = await axios.post('/generate_video', { fenge: this.fenge, projectid: this.projectid, prompt: this.shots[index].prompt, ratio: this.aspectratio }); let olditem = this.shots[index]; olditem.videoUrl = response.data.video_url; olditem.isvideo = true; this.$set(this.shots, index, olditem); } catch (error) { this.$nextTick(() => { alert('生成视频失败,请重试'); }); } finally { this.loading = false; } }, async combineVideos() { // 验证是否至少有一个分镜头有内容 const validShots = this.shots.filter(shot => (shot.imageUrl || shot.videoUrl) && shot.sayword ); if (validShots.length === 0) { alert('请确保至少一个分镜头有图片/视频和解说文字'); return; } this.loading = true; this.loadingMessage = "正在合成最终视频,请耐心等待..."; try { const configdata = { fenge: this.fenge, selectedVoice: this.selectedVoice, selectedBackgroundMusic: this.selectedBackgroundMusic, selectedTransition: this.selectedTransition }; const response = await axios.post('/combine_videos', { projectid: this.projectid, shots: this.shots, configdata: configdata }); this.finalVideoUrl = response.data.output_file; this.nextStep(); } catch (error) { this.$nextTick(() => { alert('合成视频失败,请重试'); }); } finally { this.loading = false; } }, downloadVideo() { if (this.finalVideoUrl) { const link = document.createElement('a'); link.href = this.finalVideoUrl; link.download = `${this.topic || 'video'}.mp4`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else { alert('视频尚未生成,无法下载'); } } } }); </script> </body> </html>后端python
点击查看源码
网友评论0