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