SyuanYu 2 месяцев назад
Родитель
Сommit
2d7d8028c1

+ 13 - 3
frontend/src/api.ts

@@ -102,6 +102,16 @@ export const api = {
     }
     return axios.post<{ filenames: string[] }>(`${apiUrl}/api/v1/images/sr`, formData, authHeaders(token));
   },
+  async generateZip(token: string, model: string, list: string[]) {
+    const headers = {
+      ...authHeaders(token),
+      "Content-Type": "application/json",
+    };
+
+    return axios.post(`${apiUrl}/api/v1/text2zip/gen-zip?model=${model}`, list, { ...authHeaders(token), responseType: "blob", });
+
+    // 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}`,
@@ -181,9 +191,9 @@ export const api = {
   },
   async uploadVideoContent(content: string, file: []) {
     // let content = "";
-    console.log('api content',content);
-    console.log('api file',file);
-    
+    console.log('api content', content);
+    console.log('api file', file);
+
     const formData = new FormData();
     for (let index = 0; index < file.length; index++) {
       const element = file[index];

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

@@ -72,5 +72,9 @@
     "phoneNumber": "Phone Number (optional)",
     "feedback": "Feedback",
     "greenScreen": "Green Screen",
-    "aiReporter": "AI Reporter"
+    "aiReporter": "AI Reporter",
+    "autoCreateVideoMaterials": "Auto Create Video Materials",
+    "generateZip": "Generate ZIP",
+    "paragraph": "Paragraph",
+    "addParagraph": "Add Paragraph"
 }

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

@@ -72,5 +72,9 @@
     "phoneNumber": "手機號碼(非必填)",
     "feedback": "意見回饋",
     "greenScreen": "綠幕",
-    "aiReporter": "AI 記者"
+    "aiReporter": "AI 記者",
+    "autoCreateVideoMaterials": "自動製作影片素材",
+    "generateZip": "生成 ZIP",
+    "paragraph": "段落",
+    "addParagraph": "新增段落"
 }

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

@@ -88,6 +88,11 @@ const router = createRouter({
               name: 'progress',
               component: () => import('@/views/main/Progress.vue'),
             },
+            {
+              path: 'generate-zip',
+              name: 'generate-zip',
+              component: () => import('@/views/main/GenerateZip.vue'),
+            },
             // {
             //   path: 'yt-views',
             //   name: 'yt-views',

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

@@ -379,6 +379,28 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
+    async generateZip(model: string, list: string[]) {
+      const mainStore = useMainStore();
+      try {
+        const response = (
+          await Promise.all([
+            api.generateZip(mainStore.token, model, 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 {

+ 389 - 0
frontend/src/views/main/GenerateZip.vue

@@ -0,0 +1,389 @@
+<script setup lang="ts">
+import { ref, reactive, watch, computed } from "vue";
+import { useMainStore } from "@/stores/main";
+import { required } from "@/utils";
+import { useI18n } from "vue-i18n";
+import { wsUrl } from "@/env";
+import type { VideoCreate } from "@/interfaces";
+import type { VideoUploaded } from "@/interfaces";
+import router from "@/router";
+import Dialog from "@/components/Dialog.vue";
+
+const { t } = useI18n();
+const mainStore = useMainStore();
+const WS = mainStore.videosWebSocket;
+const valid = ref(true);
+const title = ref("");
+const zipFiles = ref();
+const Form = ref();
+let selectAnchor = ref("angela");
+let selectTemplate = ref("style1");
+
+// props
+let dialog = reactive({
+  msg: "",
+  state: "info",
+  show: false,
+});
+
+watch(dialog, (newVal) => {
+  if (!newVal.show && newVal.state === "error") {
+    return;
+  } else if (!newVal.show && newVal.state === "success") {
+    router.push("/main/progress");
+  }
+});
+
+async function Submit() {
+  WS.send("subscribe");
+  await (Form as any).value.validate();
+  if (valid.value) {
+    valid.value = false;
+
+    const video_data: VideoCreate = {
+      title: title.value,
+      anchor: selectAnchor.value,
+      style: selectTemplate.value,
+      lang: "zh",
+    };
+
+    const ret: VideoUploaded = await mainStore.uploadPlot(
+      video_data,
+      zipFiles.value[0]
+    );
+
+    if (ret.accepted) {
+      dialog.msg = t("acceptZipMessage");
+      dialog.state = "success";
+      dialog.show = true;
+    } else {
+      dialog.msg = ret.error_message!;
+      dialog.state = "error";
+      dialog.show = true;
+    }
+
+    valid.value = true;
+
+    // (Form as any).value.reset();
+  }
+}
+
+let paragraphList = reactive([""]);
+
+// 新增段落
+function addParagraph() {
+  paragraphList.push("");
+}
+
+let model = ref("sd3");
+let loading = ref(false);
+const zipFile = ref(); // 儲存 ZIP 檔案
+
+async function generateZip() {
+  zipFile.value = null;
+  loading.value = true;
+  const response: any = await mainStore.generateZip(model.value, paragraphList);
+
+  zipFile.value = new Blob([response[0].data], { type: "application/zip" });
+
+  loading.value = false;
+
+  console.log("response", response);
+  console.log("zipFile.value", zipFile.value);
+}
+
+// ZIP 檔案下載
+function downloadZipFile() {
+  if (!zipFile.value) {
+    console.error("沒有 ZIP 檔案可下載");
+    return;
+  }
+
+  // 生成 Blob URL
+  const url = URL.createObjectURL(zipFile.value);
+
+  const link = document.createElement("a");
+  link.href = url;
+  link.download = "影片素材.zip"; // 設定下載檔名
+  document.body.appendChild(link);
+
+  link.click();
+  document.body.removeChild(link);
+
+  // 釋放 Blob URL
+  URL.revokeObjectURL(url);
+}
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="ma-3 pa-3">
+      <v-card-title primary-title>
+        <h3 class="card-title mb-3">{{ t("autoCreateVideoMaterials") }}</h3>
+      </v-card-title>
+      <v-card-text>
+        <v-form v-model="valid" ref="Form">
+          <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"
+          >
+            {{ t("addParagraph") }}
+          </v-btn>
+        </v-form>
+      </v-card-text>
+    </v-card>
+
+    <v-card class="ma-3 px-3">
+      <v-card-text>
+        <p>Model</p>
+        <small>sd3 is faster than flux.</small>
+        <v-radio-group
+          v-model="model"
+          color="primary"
+          class="mt-3"
+          hide-details
+        >
+          <v-radio label="flux" value="flux"></v-radio>
+          <v-radio label="sd3" value="sd3"></v-radio>
+        </v-radio-group>
+      </v-card-text>
+    </v-card>
+
+    <div class="ma-3 mt-5">
+      <v-btn
+        v-if="zipFile"
+        @click="downloadZipFile()"
+        size="large"
+        color="primary"
+        variant="outline"
+        class="w-100 mt-3"
+        prepend-icon="download"
+      >
+        {{ t("download") }}
+      </v-btn>
+
+      <v-btn
+        @click="generateZip()"
+        size="large"
+        color="primary"
+        variant="flat"
+        class="w-100"
+        :loading="loading"
+      >
+        {{ t("generateZip") }}
+      </v-btn>
+
+      <small class="d-block mt-3 text-center"
+        >此頁面生成的 ZIP
+        檔案可用於製作影片,包含圖片與段落內容。如需進一步製作影片,請點此
+        <router-link to="/main/make-video">前往製作影片</router-link></small
+      >
+    </div>
+  </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>

+ 3 - 0
frontend/src/views/main/Main.vue

@@ -111,6 +111,9 @@ const routeGuardAdmin = async (
             <v-list-item to="/main/progress" prepend-icon="list">
               <v-list-item-title>{{ t("progress") }}</v-list-item-title>
             </v-list-item>
+            <v-list-item to="/main/generate-zip" prepend-icon="video_file">
+              <v-list-item-title>{{ t("autoCreateVideoMaterials") }}</v-list-item-title>
+            </v-list-item>
             <!-- <v-list-item to="/main/make-image" prepend-icon="image">
               <v-list-item-title>圖片優化</v-list-item-title>
             </v-list-item> -->

+ 10 - 2
frontend/src/views/main/Upload.vue

@@ -29,7 +29,7 @@ let dialog = reactive({
 });
 
 const anchorList = reactive([
-{
+  {
     anchor_id: "african2",
     name: "African",
   },
@@ -329,6 +329,10 @@ async function Submit() {
             prepend-icon="folder_zip"
           ></v-file-input>
 
+          <div class="mb-5 ms-10">
+            <router-link to="/main/generate-zip">還沒有準備好 ZIP 檔案?請點此製作素材</router-link>
+          </div>
+
           <!-- <v-select
             v-model="anchorLang"
             :items="items"
@@ -584,7 +588,11 @@ async function Submit() {
   </v-container>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
+a {
+  letter-spacing: 1px;
+}
+
 .anchor-list {
   ul {
     display: grid;