python+vue编写一个ai生成电影短剧故事片视频代码

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

点击查看源码

{{collectdata}}

网友评论0

云产品购物券