SyuanYu 2 months ago
parent
commit
932f68a855

+ 9 - 1
frontend/src/api.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import { apiUrl } from "@/env";
-import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData, SimplePaymentData } from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData, SimplePaymentData, genVideoData } from "@/interfaces";
 import type { StringOptionsWithImporter } from "sass";
 
 function authHeaders(token: string) {
@@ -112,6 +112,14 @@ export const api = {
 
     // return axios.post(`${apiUrl}/api/v1/text2zip/gen-zip?model=${model}`, list, authHeaders(token));
   },
+  async generateVideo(data: genVideoData, list: string[]) {
+    console.log('generateVideo data', data);
+    console.log('generateVideo list', list);
+
+    return axios.post(`${apiUrl}/api/v1/text2zip/gen-video?model=${data.model}&email=${data.email}&lang=${data.lang}`, list);
+
+    // return axios.post(`${apiUrl}/api/v1/text2zip/gen-zip?model=${model}`, list, authHeaders(token));
+  },
   // async getImage(token: string, data: ImageDownload) {
   //   axios({
   //     url: `${apiUrl}/api/v1/images/sr?stored_file_name=${data.stored_file_name}&file_name=${data.file_name}`,

+ 1 - 0
frontend/src/components/Navbar.vue

@@ -15,6 +15,7 @@ let menu = reactive([
   { title: "ytViews", link: "/yt-views" },
   { title: "aiReporter", link: "/ai-reporter" },
   { title: "genSeo", link: "/gen-seo" },
+  { title: "autoCreateVideo", link: "/gen-video" },
   // { title: "實體卡儲值", link: "/qrcode" },
 ]);
 

+ 6 - 0
frontend/src/interfaces/index.ts

@@ -115,4 +115,10 @@ export interface VideoContent {
   title: string;
   text: string;
   caption: [];
+}
+
+export interface genVideoData {
+  model: string;
+  email: string;
+  lang: string;
 }

+ 1 - 0
frontend/src/language/en.json

@@ -74,6 +74,7 @@
     "greenScreen": "Green Screen",
     "aiReporter": "AI Reporter",
     "genSeo": "Gen SEO",
+    "autoCreateVideo": "Auto Create Video",
     "autoCreateVideoMaterials": "Auto Create Video Materials",
     "generateZip": "Generate ZIP",
     "paragraph": "Paragraph",

+ 1 - 0
frontend/src/language/zh.json

@@ -74,6 +74,7 @@
     "greenScreen": "綠幕",
     "aiReporter": "AI 記者",
     "genSeo": "算力傳媒",
+    "autoCreateVideo": "自動製作影片",
     "autoCreateVideoMaterials": "自動製作影片素材",
     "generateZip": "生成 ZIP",
     "paragraph": "段落",

+ 5 - 0
frontend/src/router/index.ts

@@ -63,6 +63,11 @@ const router = createRouter({
           name: 'test-yt-views',
           component: () => import('@/views/TestYTViews.vue'),
         },
+        {
+          path: 'gen-video',
+          name: 'gen-video',
+          component: () => import('@/views/GenVideo.vue'),
+        },
         {
           path: 'main',
           name: 'main',

+ 22 - 1
frontend/src/stores/main.ts

@@ -4,7 +4,7 @@ import { api } from "@/api"
 import router from "@/router"
 import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
 import type { AppNotification } from '@/interfaces';
-import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData, SimplePaymentData, VideoContent } from '@/interfaces';
+import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded, YTViewsUserData, PaymentData, SimplePaymentData, VideoContent, genVideoData } from '@/interfaces';
 import i18n from '@/plugins/i18n'
 import { wsUrl } from "@/env";
 
@@ -411,6 +411,27 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
+    async generateVideo(data: genVideoData, list: string[]) {
+      try {
+        const response = (
+          await Promise.all([
+            api.generateVideo(data, list),
+            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
+          ]).then(data => {
+            return data;
+          })
+        );
+
+        return response;
+
+        // mainStore.addNotification({
+        //   content: i18n.global.t("fileReceived"),
+        //   color: "success",
+        // })
+      } catch (error) {
+        await mainStore.checkApiError(error);
+      }
+    },
     // async getImage(data: ImageDownload) {
     //   const mainStore = useMainStore();
     //   try {

+ 364 - 0
frontend/src/views/GenVideo.vue

@@ -0,0 +1,364 @@
+<script setup lang="ts">
+import { ref, reactive, watch } from "vue";
+import { useMainStore } from "@/stores/main";
+import { required, emailRules } from "@/utils";
+import { useI18n } from "vue-i18n";
+import { wsUrl } from "@/env";
+import router from "@/router";
+import Navbar from "@/components/Navbar.vue";
+
+const { t } = useI18n();
+const mainStore = useMainStore();
+const WS = mainStore.videosWebSocket;
+const valid = ref(true);
+const Form = ref();
+
+let dialog = ref(false);
+let paragraphList = reactive([""]);
+
+// 新增段落
+function addParagraph() {
+  paragraphList.push("");
+}
+
+let data = reactive({
+  model: "sd3",
+  email: "",
+  lang: "zh",
+});
+
+// let model = ref("sd3");
+let loading = ref(false);
+const zipFile = ref(); // 儲存 ZIP 檔案
+
+async function generateVideo() {
+  console.log("data", data);
+
+  loading.value = true;
+  const response: any = await mainStore.generateVideo(data, paragraphList);
+
+  if (response[0].status === 200) {
+    loading.value = false;
+    alert("影片已開始製作,請留意來自我們的信件,謝謝!");
+    dialog.value = false;
+  }
+
+  console.log("response", response);
+}
+</script>
+
+<template>
+  <Navbar />
+
+  <v-container fluid class="mt-16">
+    <v-row class="justify-center">
+      <v-col cols="10">
+        <v-card class="ma-3 py-8 px-16">
+          <v-card-title class="text-center" primary-title>
+            <h3 class="card-title mb-3">
+              自動製作影片(試用版)<br />
+              <small>Gen Video</small>
+            </h3>
+          </v-card-title>
+
+          <v-card-text>
+            <v-form v-model="valid" ref="Form">
+              <div class="d-flex justify-center mb-10">
+                <div class="pe-10 border-e-sm">
+                  <p>Model</p>
+                  <v-radio-group
+                    v-model="data.model"
+                    color="primary"
+                    class="mt-2"
+                    hide-details
+                    inline
+                    style="margin-left: -10px"
+                  >
+                    <v-radio label="flux" value="flux"></v-radio>
+                    <v-radio label="sd3" value="sd3" class="ms-2"></v-radio>
+                  </v-radio-group>
+                </div>
+
+                <div class="ms-10">
+                  <p>使用語言</p>
+                  <v-radio-group
+                    v-model="data.lang"
+                    color="primary"
+                    class="mt-2"
+                    hide-details
+                    inline
+                    style="margin-left: -10px"
+                  >
+                    <v-radio label="中文" value="zh"></v-radio>
+                    <v-radio label="en" value="en" class="ms-2"></v-radio>
+                  </v-radio-group>
+                </div>
+              </div>
+
+              <v-text-field
+                v-for="(item, index) in paragraphList"
+                :label="`${t('paragraph')} ${index + 1}`"
+                v-model="paragraphList[index]"
+                :rules="required()"
+                prepend-icon="title"
+              >
+              </v-text-field>
+
+              <v-btn
+                @click="addParagraph()"
+                size="large"
+                color="primary"
+                variant="tonal"
+                class="w-100 text-white mt-5 mb-2"
+              >
+                {{ t("addParagraph") }}
+              </v-btn>
+            </v-form>
+          </v-card-text>
+        </v-card>
+      </v-col>
+    </v-row>
+
+    <div class="ma-3 mt-10 d-flex justify-center">
+      <v-btn
+        @click="dialog = true"
+        size="large"
+        color="primary"
+        variant="flat"
+        class="h-auto"
+      >
+        <span class="d-flex flex-column py-2">
+          <p class="mb-1">生成影片</p>
+          <small>Generate Video</small>
+        </span>
+      </v-btn>
+    </div>
+
+    <v-dialog v-model="dialog" width="700">
+      <v-card class="pa-5">
+        <v-card-title class="font-weight-bold text-center">
+          請輸入您的信箱
+        </v-card-title>
+
+        <v-card-text>
+          <v-text-field
+            v-model="data.email"
+            :rules="emailRules()"
+            label="電子郵件"
+            required
+            variant="solo"
+          ></v-text-field>
+
+          <p class="py-3">影片完成後我們會將影片寄送至您的信箱。</p>
+        </v-card-text>
+
+        <v-card-actions>
+          <v-btn
+            @click="generateVideo()"
+            class="mx-auto"
+            color="primary"
+            variant="flat"
+            size="large"
+            :loading="loading"
+          >
+            {{ t("send") }}
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </v-container>
+</template>
+
+<style lang="scss">
+.anchor-list {
+  ul {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(185px, max-content));
+    grid-gap: 20px;
+    justify-content: center;
+    padding: initial;
+    li {
+      list-style-type: none;
+    }
+  }
+  img {
+    width: 190px;
+    height: 155px;
+    object-fit: cover;
+  }
+
+  .v-card--variant-elevated {
+    box-shadow: 0px 2px 5px 1px
+        var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)),
+      0px 1px 1px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)),
+      0px 1px 3px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.12));
+  }
+
+  .v-card-item {
+    padding: 0;
+    text-align: center;
+    .v-card-title {
+      font-size: 18px;
+    }
+  }
+  .bg-success {
+    background: linear-gradient(
+      -225deg,
+      rgb(234, 84, 19) 35%,
+      rgb(178, 69, 146) 100%
+    ) !important;
+  }
+  .v-expansion-panel-text__wrapper {
+    padding: 0 !important;
+  }
+}
+
+.anchor-list,
+.template-list {
+  padding-left: 40px;
+  .v-expansion-panel-title {
+    height: 55px;
+    min-height: 0;
+  }
+}
+
+.template-list {
+  img {
+    width: 100%;
+    height: 180px;
+  }
+  .choose-btn {
+    padding: 5px;
+    position: absolute;
+    right: 8px;
+    bottom: 13px;
+    background: #ccc;
+    border-radius: 100px;
+  }
+  .active-color {
+    background: #ea5413;
+  }
+}
+
+.step-list {
+  list-style: none;
+  img {
+    width: 100%;
+    max-width: 1000px;
+  }
+  li {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    font-size: 16px;
+    p {
+      line-height: 32px;
+    }
+    h4 {
+      margin: 20px auto;
+      color: #ea5413;
+      font-weight: bold;
+      text-align: center;
+      line-height: 34px;
+      font-size: 20px;
+    }
+  }
+
+  .link-btn {
+    display: inline-block;
+    padding: 12px 20px;
+    margin-top: 25px;
+    border-radius: 100px;
+    text-decoration: none;
+    color: #fff;
+    background: #ea5413;
+    transition: all 0.3s;
+    &:hover {
+      opacity: 0.8;
+    }
+  }
+
+  .point-list {
+    display: flex;
+    flex-direction: column;
+    align-items: baseline;
+    margin-left: 40px;
+  }
+
+  .point-content {
+    .base,
+    .advanced {
+      padding: 40px;
+      margin-top: 50px;
+      max-width: 1000px;
+      letter-spacing: 1px;
+      border-radius: 5px;
+    }
+
+    .base {
+      border: 4px solid #ea5413;
+    }
+
+    .advanced {
+      border: 4px dashed #ea5413;
+    }
+
+    ul {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-start;
+
+      li {
+        margin: 5px 0;
+      }
+    }
+
+    h5 {
+      margin-bottom: 20px;
+      text-align: center;
+      font-size: 1.25rem;
+    }
+
+    hr {
+      margin: 30px;
+      border-color: #f2f2f2;
+      opacity: 0.3;
+    }
+  }
+
+  .excerpt::before {
+    content: "";
+    font-weight: bold;
+    display: inline-block;
+    border: 5px solid #ea5413;
+    border-radius: 20px;
+    margin-right: 10px;
+    margin-bottom: 2px;
+  }
+}
+
+.img-disabled {
+  position: relative;
+  z-index: 999;
+  background-color: #ccc;
+  margin-bottom: -5px;
+  img {
+    opacity: 0.7;
+  }
+  p {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    color: #fff;
+    transform: translate(-50%, -50%);
+    font-size: 16px;
+    text-align: center;
+    letter-spacing: 1px;
+    text-shadow: 2px 2px 6px #000;
+  }
+}
+
+.v-card--disabled > :not(.v-card__loader) {
+  opacity: 1 !important;
+}
+</style>