Forráskód Böngészése

Merge branch 'master' of http://git.choozmo.com:3000/ai-anchor/video-maker

tomoya 1 éve
szülő
commit
4800247a5c

+ 0 - 1
backend/app/app/aianchor/__init__.py

@@ -1 +0,0 @@
-from .videomaker import *

+ 70 - 0
backend/app/app/aianchor/utils2.py

@@ -0,0 +1,70 @@
+import pandas as pd
+from pathlib import Path
+import zipfile
+from io import BytesIO
+from chardet.universaldetector import UniversalDetector
+
+DEFAULT_ENCODING = "utf-8"
+
+class VideoMakerError(Exception):
+  pass
+
+def guess_codec(filenames: list) -> str:
+  codec_detector = UniversalDetector()
+  for filename in filenames:
+    codec_detector.feed(filename.encode('cp437'))
+    if codec_detector.done:
+      break
+
+  result = codec_detector.close()
+  encoding = result.get("encoding")
+  return encoding or DEFAULT_ENCODING
+
+def check_zip(zip_filepath:str):
+  path = Path(zip_filepath)
+  with zipfile.ZipFile(str(path)) as zf:
+    filenames = [x for x in zf.namelist() if not x.endswith('/')]
+    result = guess_codec(filenames)
+    true_filenames = [x.encode('cp437').decode(result) for x in zf.namelist() if not x.endswith('/')]
+    print(true_filenames)
+    scenarios_files = [(x, i) for i, x in enumerate(true_filenames) if Path(x).suffix in [".xlsx", ".csv"] and not Path(x).name.startswith("._") and Path(x).stem != "style"]
+    print(scenarios_files)
+    
+    if len(scenarios_files) == 0:
+      raise VideoMakerError("no excel or csv file in zip.")
+    if len(scenarios_files) > 1:
+      raise VideoMakerError("too many excel or csv file in zip.")
+    f = zf.read(filenames[scenarios_files[0][1]])
+    if Path(scenarios_files[0][0]).suffix == ".xlsx":
+      table = pd.read_excel(BytesIO(f), dtype=object)
+    elif Path(scenarios_files[0][0]).suffix == ".csv":
+      table = pd.read_csv(BytesIO(f), dtype=object)
+    table.reset_index(inplace=True)
+    print(table)
+    
+    stems = [Path(x).stem for x in true_filenames]
+    for i in range(len(table)):
+      # excel 裡的圖檔跟zip裡的檔案要一致
+      if not table.loc[i, ['素材']].isna().item():
+        img =  table.loc[i, ['素材']].item()
+        print(img)
+
+        img_files = [x.strip() for x in img.split(',')]
+        for img in img_files:
+          print(img)
+          n = stems.count(img)
+          if n == 0:
+            raise VideoMakerError(f"{img}: no such media file in zip.")
+          elif n > 1:
+            raise VideoMakerError(f'too many same name media files as {img} in zip')
+      
+      # 需要tts文字或音檔
+      if not table.loc[i, ['字幕']].isna().item():
+        if not '音檔' in table.columns or table.loc[i, ['音檔']].isna().item():
+          raise VideoMakerError(f'text or voice file is needed at scene {i+1}.')
+        voice_file = table.loc[i, ['音檔']].item()
+        n = stems.count(voice_file)
+        if n != 1:
+          raise VideoMakerError(f"voice file is can't find is zip at scene {i+1}.")
+  
+  return True

+ 24 - 4
backend/app/app/api/api_v1/endpoints/videos.py

@@ -12,6 +12,7 @@ from app.api import deps
 
 from app.core.celery_app import celery_app
 from app.core.config import settings
+from app.aianchor.utils2 import check_zip, VideoMakerError
 from pathlib import Path
 from app.db.session import SessionLocal
 
@@ -45,6 +46,7 @@ def get_video_list(
 @router.post("/test")
 def test(
     *,
+    db: Session = Depends(deps.get_db),
     title: str,
     anchor_id: int,
     lang_id: int,
@@ -52,7 +54,15 @@ def test(
 ) -> Any:
     video_data = {"title":title, "anchor_id":anchor_id, "lang_id":lang_id}
     print(video_data)
-    task = celery_app.send_task("app.worker.make_video_test",  kwargs=video_data, )
+    filename = crud.video.generate_file_name(db=db, n=20)
+    video_create = schemas.VideoCreate(title=title, progress_state="PENDING", stored_filename=filename)
+    video = crud.video.create_with_owner(db=db, obj_in=video_create, owner_id=current_user.id, )
+    return_msg = {"video_message":"accepted"}
+    video_data = jsonable_encoder(video)
+    video_data['membership_status'] = current_user.membership_status
+    video_data['available_time'] = current_user.available_time
+    video_data['video_id'] = video_data['id']
+    task = celery_app.send_task("app.worker.make_video_test",  kwargs=video_data)
     print(task)
     return "ok"
 
@@ -73,17 +83,26 @@ def upload_plot(
     print(title)
     print(upload_file.filename)
     filename = crud.video.generate_file_name(db=db, n=20)
-    
+    filepath = str(Path(BACKEND_ZIP_STORAGE).joinpath(filename+".zip"))
     try:
-        with open(str(Path(BACKEND_ZIP_STORAGE).joinpath(filename+".zip")), 'wb') as f:
+        with open(filepath, 'wb') as f:
             while contents := upload_file.file.read(1024 * 1024):
                 f.write(contents)
     except Exception as e:
         print(e, type(e))
-        return {"error": str(e)}
+        error_msg = {"error_message": str(e)}
+        return JSONResponse(error_msg)
     finally:
         upload_file.file.close()
+    try:
+      if check_zip(filepath):
+        print("passed check_zip")
+    except VideoMakerError as e:
+      print(e)
+      error_msg = {"accepted": False, "error_message":f'{e}'}
+      return JSONResponse(error_msg)
     
+    return_msg = {"accepted_message":"accepted"}
 
     '''
     zip_filename = video.stored_file_name+".zip"
@@ -93,6 +112,7 @@ def upload_plot(
     print(r.returncode)
     celery_app.send_task("app.worker.make_video", args=[video.id, video.stored_file_name, current_user.id, anchor_id, current_user.membership_status, current_user.available_time])
     '''
+  
     video_create = schemas.VideoCreate(title=title, progress_state="PENDING", stored_filename=filename)
     video = crud.video.create_with_owner(db=db, obj_in=video_create, owner_id=current_user.id)
     return_msg = {"video_message":"accepted"}

+ 2 - 2
backend/app/app/models/video.py

@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING
 
 from sqlalchemy import Column, ForeignKey, Integer, String, Enum, DateTime
 from sqlalchemy.orm import relationship
-
+from sqlalchemy.sql import func
 from app.db.base_class import Base
 
 
@@ -17,7 +17,7 @@ class Video(Base):
   progress_state = Column(String(10), 
                     ForeignKey("progress.state", ondelete="RESTRICT", onupdate="CASCADE"),
                     default="waiting")
-  created_datetime = Column(DateTime)
+  created_datetime = Column(DateTime(timezone=True), default=func.now())
   length = Column(Integer)
   owner_id = Column(Integer, ForeignKey("user.id"))
   owner = relationship("User", back_populates="videos")

+ 2 - 2
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 } from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded } from "@/interfaces";
 
 function authHeaders(token: string) {
   return {
@@ -84,7 +84,7 @@ export const api = {
     formData.append("anchor_id", video_data.anchor_id.toString())
     formData.append("lang_id", video_data.lang_id.toString())
     formData.append("upload_file", file)
-    return axios.post<{ msg: string }>(`${apiUrl}/api/v1/videos/`, formData, authHeaders(token));
+    return axios.post<VideoUploaded>(`${apiUrl}/api/v1/videos/`, formData, authHeaders(token));
   },
   async uploadImage(token: string, file: File[]) {
     const formData = new FormData();

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

@@ -55,6 +55,12 @@ export interface VideoCreate {
   lang_id: number;
 }
 
+export interface VideoUploaded {
+  accepted: boolean;
+  error_message: string | null;
+  video_info: Video | null;
+}
+
 export interface ArticleCreate {
   title: string;
   link: string;

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

@@ -58,5 +58,6 @@
     "editUserProfile": "Edit User Profile",
     "incorrectUsername": "Incorrect username",
     "sendingEmail": "Sending password recovery email",
-    "passwordMailSent": "Password recovery email sent"
+    "passwordMailSent": "Password recovery email sent",
+    "acceptZipMessage": "Video processing takes about 5-10 minutes, please be patient"
 }

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

@@ -58,5 +58,6 @@
     "editUserProfile": "編輯資料",
     "incorrectUsername": "使用者名稱不正確",
     "sendingEmail": "傳送電子郵件中",
-    "passwordMailSent": "重置密碼電子郵件已傳送"
+    "passwordMailSent": "重置密碼電子郵件已傳送",
+    "acceptZipMessage": "影片處理需要約 5-10 分鐘,敬請耐心等候"
 }

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

@@ -129,6 +129,18 @@ const router = createRouter({
                   component: () => import(
                     /* webpackChunkName: "main-admin-users-create" */ '@/views/main/admin/TestCelery.vue'),
                 },
+                {
+                  path: 'test-ecpay',
+                  name: 'test-ecpay',
+                  component: () => import(
+                    /* webpackChunkName: "main-admin-users-create" */ '@/views/main/admin/TestECPay.vue'),
+                },
+                {
+                  path: 'test-style-preview',
+                  name: 'test-style-preview',
+                  component: () => import(
+                    /* webpackChunkName: "main-admin-users-create" */ '@/views/main/admin/TestStylePreview.vue'),
+                },
               ],
             },
           ],

+ 4 - 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 } from '@/interfaces';
+import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded } from '@/interfaces';
 import i18n from '@/plugins/i18n'
 import { wsUrl } from "@/env";
 
@@ -324,9 +324,12 @@ export const useMainStore = defineStore("MainStoreId", {
           color: "success",
         })
         this.actionGetVideos();
+        return response[0].data;
       } catch (error) {
         await mainStore.checkApiError(error);
       }
+      const ret: VideoUploaded = {accepted:false, error_message:"api error", video_info: null}
+      return ret
     },
     async uploadImage(file: File[]) {
       const mainStore = useMainStore();

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

@@ -149,6 +149,18 @@ const routeGuardAdmin = async (
             >
               <v-list-item-title>Test Celery</v-list-item-title>
             </v-list-item>
+            <v-list-item
+              to="/main/admin/test-ecpay"
+              prepend-icon="payments"
+            >
+              <v-list-item-title>Test ECPay</v-list-item-title>
+            </v-list-item>
+            <v-list-item
+              to="/main/admin/test-style-preview"
+              prepend-icon="preview"
+            >
+              <v-list-item-title>Test Style Preview</v-list-item-title>
+            </v-list-item>
           </v-list>
         </v-sheet>
         <!-- <v-spacer></v-spacer> -->

+ 15 - 8
frontend/src/views/main/Upload.vue

@@ -5,14 +5,8 @@ import { required } from "@/utils";
 import { useI18n } from "vue-i18n";
 import { wsUrl } from "@/env";
 import type { VideoCreate } from "@/interfaces";
-// import Dialog from "@/components/Dialog.vue";
+import type { VideoUploaded } from "@/interfaces";
 
-// props
-// let dialog = reactive({
-//   msg: "影片處理需要約 5-10 分鐘,敬請耐心等候",
-//   state: "info",
-//   show: false,
-// });
 
 const { t } = useI18n();
 const mainStore = useMainStore();
@@ -24,6 +18,13 @@ const Form = ref();
 let anchor = ref(0);
 let templateId = ref(0);
 
+// props
+let dialog = reactive({
+  msg: "",
+  state: "info",
+  show: false,
+});
+
 const anchorList = reactive([
   {
     anchor_id: 0,
@@ -189,7 +190,13 @@ async function Submit() {
       lang_id: 0,
     };
 
-    await mainStore.uploadPlot(video_data, zipFiles.value[0]);
+    const ret:VideoUploaded = await mainStore.uploadPlot(video_data, zipFiles.value[0]);
+    if (ret.accepted) {
+      dialog.msg = t("acceptZipMessage")
+    }
+    else {
+      dialog.msg = ret.error_message!
+    }
     valid.value = true;
     (Form as any).value.reset();
   }

+ 55 - 0
frontend/src/views/main/admin/TestECPay.vue

@@ -0,0 +1,55 @@
+<template>
+  <v-container fluid>
+    <v-card class="ma-3 pa-3">
+      <v-card-title primary-title>
+        <div class="headline primary--text">Test ECPay</div>
+      </v-card-title>
+      <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn @click="ECPaySubmit">
+              Send
+            </v-btn>
+      </v-card-actions>
+    </v-card>
+  </v-container>
+</template>
+<script setup lang="ts">
+import { ref} from 'vue';
+import { required } from '@/utils';
+import { useAdminStore } from '@/stores/admin';
+import axios from "axios"; 
+
+async function ECPaySubmit() {
+  console.log("ECPay button pushed")
+  const formData = new URLSearchParams();
+  formData.append("MerchantID", "3002607") //必填
+  formData.append("MerchantTradeNo", "AAA000") //必填
+  formData.append("MerchantTradeDate", "2023/05/15 10:35:10") //必填
+  formData.append("PaymentType", "aio") //必填
+  formData.append("TotalAmount", "600") //必填
+  formData.append("TradeDesc", "choozmo SaaS") //必填
+  formData.append("ItemName", "charge600") //必填
+  formData.append("ReturnURL", "https:cloud.choozmo.com") //必填
+  formData.append("ChoosePayment", "ALL") //必填
+  formData.append("CheckMacValue", "choaho")
+  formData.append("EncryptType", "1") //必填
+  formData.append("StoreID", "")
+  formData.append("ClientBackURL", "")
+  formData.append("ItemURL", "")
+  formData.append("Remark", "")
+  formData.append("ChooseSubPayment", "")
+  formData.append("OrderResultURL", "")
+  formData.append("NeedExtraPaidInfo", "")
+  formData.append("IgnorePayment", "")
+  formData.append("PlatformID", "")
+  formData.append("IgnorePayment", "")
+  formData.append("CustomField1 ", "")
+  formData.append("CustomField2", "")
+  formData.append("CustomField3", "")
+  formData.append("CustomField4", "")
+  formData.append("Language", "")
+  return axios.post("https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5", formData)
+  
+
+}
+</script>