Ver código fonte

Merge remote-tracking branch 'origin/master' into front-dev

SyuanYu 1 mês atrás
pai
commit
8586e53ebe

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


+ 39 - 0
backend/app/app/aianchor/text2image.py

@@ -0,0 +1,39 @@
+from PIL import Image,ImageDraw,ImageFont
+import re
+
+def trim_punctuation(s):
+  pat_block = u'[^\u4e00-\u9fff0-9a-zA-Z]+'
+  pattern = u'([0-9]+{0}[0-9]+)|{0}'.format(pat_block)
+  res = re.sub(pattern, lambda x: x.group(1) if x.group(1) else u" " ,s)
+    
+def text2image(content, save_target, **args):
+  unicode_text = trim_punctuation(content)
+  font = ''
+  if args["lang"]=='zh':
+      font = ImageFont.truetype(font="/FONTS/NotoSansTC-Regular.otf", size=args["size"])
+  elif args["lang"]=='中文':
+      font = ImageFont.truetype(font="/FONTS/NotoSansTC-Regular.otf", size=args["size"])
+  elif args["lang"]=='日文':
+      font = ImageFont.truetype(font="/FONTS/NotoSansJP-Regular.otf", size=args["size"])
+  elif args["lang"]=='泰文':
+      font = ImageFont.truetype(font="/FONTS/NotoSansThai-Regular.ttf", size=args["size"])
+  elif args["lang"]=='馬來文':
+      font = ImageFont.truetype(font="/FONTS/NotoSansMalayalam-Regular.ttf", size=args["size"])
+  else :
+      font = ImageFont.truetype(font="/FONTS/NotoSans-Regular.ttf", size=args["size"])
+  W, H = (1280,720)
+  canvas = Image.new('RGBA', (W, H), "#00000000")
+  draw = ImageDraw.Draw(canvas)
+  text= content
+
+  w = draw.textlength(content,font = font)
+  #draw.text(((W-w)/2,0), text,'black', font)
+  #text_border(draw,(W-w)/2,0,text,font,(83, 49, 0),'black')
+  fill = tuple(args["fill"]) if type(args["fill"]) == list else args["fill"]
+  stroke_fill = tuple(args["stroke_fill"]) if type(args["stroke_fill"]) == list else args["stroke_fill"]
+  draw.multiline_text(((W-w)/2,0), text,font=font, fill=fill, stroke_fill=stroke_fill, stroke_width=args["stroke_width"], align="right")
+  canvas.save(str(save_target), "PNG")
+    
+    
+if __name__ == "__main__":
+  text2image("This is test text. 這是測試文本。", "./black_15.png", lang="zh", size=15, fill='black', stroke_fill='white', stroke_width=0, align="right")

+ 3 - 3
backend/app/app/aianchor/utils2.py

@@ -56,8 +56,7 @@ def check_zip(zip_filepath:str):
       # excel 裡的圖檔跟zip裡的檔案要一致
       if not table.loc[i, ['素材']].isna().item():
         img =  table.loc[i, ['素材']].item()
-        img = str(img)
-        img_files = [x.strip() for x in img.split(',')]
+        img_files = [x.strip() for x in str(img).split(',')]
         for img in img_files:
           if Path(img).suffix:
             n = len([x for x in true_filenames if x[0] == img])
@@ -128,7 +127,7 @@ def check_and_extract_zip(zip_filepath:str, working_dirpath:str):
       # excel 裡的圖檔跟zip裡的檔案要一致
       if not table.loc[i, ['素材']].isna().item():
         img =  table.loc[i, ['素材']].item()
-        img_files = [x.strip() for x in img.split(',')]
+        img_files = [x.strip() for x in str(img).split(',')]
         for img in img_files:
           if Path(img).suffix:
             target_filenames = [x for x in true_filenames if x[0] == img]
@@ -145,6 +144,7 @@ def check_and_extract_zip(zip_filepath:str, working_dirpath:str):
               with zf.open(filenames[target_filenames[0][1]], 'r') as origin_file:  # 開啟原檔案
                 shutil.copyfileobj(origin_file, output_file)  # 將原檔案內容複製到新檔案
           else:
+            continue
             raise VideoMakerError(f"{target_filenames[0][0]} already exists.")
         
       # 需要tts文字或音檔

+ 5 - 2
backend/app/app/api/api_v1/api.py

@@ -1,9 +1,10 @@
 from fastapi import APIRouter
 
 from app.api.api_v1.endpoints import  login, users, utils, videos, images, reputations, ser_no
-from app.api.api_v1.endpoints import ytviewspayment, payment
+from app.api.api_v1.endpoints import ytviewspayment, payment, simplepayment
 from app.api.api_v1.endpoints import ecpay
 from app.api.api_v1.endpoints import heartbeat
+from app.api.api_v1.endpoints import text2zip
 
 api_router = APIRouter()
 api_router.include_router(login.router, tags=["login"])
@@ -15,5 +16,7 @@ api_router.include_router(reputations.router, prefix="/reputations", tags=["repu
 api_router.include_router(ser_no.router, prefix="/ser_nos", tags=["serial numbers"])
 api_router.include_router(ytviewspayment.router, prefix="/payment", tags=["yt views payment"])
 api_router.include_router(payment.router, prefix="/payment", tags=["payment"])
+api_router.include_router(simplepayment.router, prefix="/payment", tags=["simple payment"])
 api_router.include_router(ecpay.router, prefix="/ecpay", tags=["ecpay"])
-api_router.include_router(heartbeat.router, prefix="/heartbeat", tags=["heartbeat"])
+api_router.include_router(heartbeat.router, prefix="/heartbeat", tags=["heartbeat"])
+api_router.include_router(text2zip.router, prefix="/text2zip", tags=["text2zip"] )

+ 327 - 0
backend/app/app/api/api_v1/endpoints/simplepayment.py

@@ -0,0 +1,327 @@
+from typing import Any, List, Optional
+from datetime import datetime
+
+from fastapi import APIRouter, Body, Depends, HTTPException, Form, status, Response
+from fastapi.encoders import jsonable_encoder
+from pydantic.networks import EmailStr, HttpUrl
+from sqlalchemy.orm import Session
+
+import app.crud as crud
+import app.models as models
+import app.schemas as schemas 
+from app.api import deps
+from app.core.config import settings
+from app.core.ecpay_payment_sdk import ECPayPaymentSdk
+from app.utils import send_new_account_email
+from pydantic import BaseModel
+import requests
+from random import choice
+import string
+import json
+
+router = APIRouter()
+
+@router.post('/simple-ecpay-test-payment')
+def ecpay_payment(
+  *,
+  db: Session = Depends(deps.get_db),
+  user_data: schemas.SimplePayCreate,
+  lang: str=''
+):  
+    MerchantTradeNo = 'test'+datetime.now().strftime("NO%Y%m%d%H%M%S")
+    remark = {'MerchantTradeNo':MerchantTradeNo}
+    remark_string = json.dumps(remark, ensure_ascii=False)
+    simplepay = crud.simplepay.create_with_payment_data(db, 
+                                                        obj_in=user_data, 
+                                                        epayment='ecpay',
+                                                        remark=remark_string)
+    order_params = {
+        'MerchantTradeNo': MerchantTradeNo,
+        'StoreID': 'SaaS',
+        'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
+        'PaymentType': 'aio',
+        'TotalAmount': user_data.amount,
+        'TradeDesc': user_data.tradeDesc,
+        'ItemName': user_data.item,
+        'ReturnURL': 'https://cloud.choozmo.com/api/v1/payment/simple-pay-ecpay-result-return',
+        'ChoosePayment': 'ALL',
+        'ClientBackURL': 'https://cloud.choozmo.com/test-simple-pay',
+        'ItemURL': 'https://cloud.choozmo.com/test-simple-pay',
+        'Remark': '',
+        'ChooseSubPayment': '',
+        'OrderResultURL': '',
+        'NeedExtraPaidInfo': 'Y',
+        'DeviceSource': '',
+        'IgnorePayment': 'ATM#CVS#BARCODE',
+        'PlatformID': '',
+        'InvoiceMark': 'N',
+        'CustomField1': str(simplepay.id),
+        'CustomField2': user_data.taxID if user_data.taxID else '',
+        'CustomField3': '',
+        'CustomField4': '',
+        'EncryptType': 1,
+        'Language': lang,
+    }
+
+    extend_params_1 = {
+        'ExpireDate': 3,
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_2 = {
+        'StoreExpireDate': 7,
+        'Desc_1': '',
+        'Desc_2': '',
+        'Desc_3': '',
+        'Desc_4': '',
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_3 = {
+        'BindingCard': 0,
+        'MerchantMemberID': '',
+    }
+
+    extend_params_4 = {
+        'Redeem': 'N',
+        'UnionPay': 0,
+    }
+
+    inv_params = {
+        'RelateNumber': MerchantTradeNo, # 特店自訂編號
+        # 'CustomerID': 'TEA_0000001', # 客戶編號
+        'CustomerIdentifier': user_data.taxID if user_data.taxID else '', # 統一編號
+        'CustomerName': user_data.name,
+        # 'CustomerAddr': '客戶地址',
+        # 'CustomerPhone': '0912345678', # 客戶手機號碼
+        'CustomerEmail': user_data.email,
+        # 'ClearanceMark': '2', # 通關方式
+        # 'TaxType': '1', # 課稅類別
+        # 'CarruerType': '', # 載具類別
+        # 'CarruerNum': '', # 載具編號
+        # 'Donation': '1', # 捐贈註記
+        # 'LoveCode': '168001', # 捐贈碼
+        # 'Print': '1',
+        # 'InvoiceItemName': '測試商品1|測試商品2',
+        # 'InvoiceItemCount': '2|3',
+        # 'InvoiceItemWord': '個|包',
+        # 'InvoiceItemPrice': '35|10',
+        # 'InvoiceItemTaxType': '1|1',
+        # 'InvoiceRemark': '測試商品1的說明|測試商品2的說明',
+        # 'DelayDay': '0', # 延遲天數
+        # 'InvType': '07', # 字軌類別
+    }
+
+    # 建立實體
+    ecpay_payment_sdk = ECPayPaymentSdk(
+        MerchantID='3002607',
+        HashKey='pwFHCqoQZGmho4w6',
+        HashIV='EkRm7iFT261dpevs'
+    )
+
+    # 合併延伸參數
+    order_params.update(extend_params_1)
+    order_params.update(extend_params_2)
+    order_params.update(extend_params_3)
+    order_params.update(extend_params_4)
+
+    # 合併發票參數
+    order_params.update(inv_params)
+    try:
+        # 產生綠界訂單所需參數
+        final_order_params = ecpay_payment_sdk.create_order(order_params)
+
+        # 產生 html 的 form 格式
+        action_url = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'  # 測試環境
+        # action_url = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5' # 正式環境
+        html = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
+        
+        return html
+    except Exception as error:
+        print('An exception happened: ' + str(error))
+
+@router.post('/simpley-ecpay-payment')
+def ecpay_payment(
+  *,
+  db: Session = Depends(deps.get_db),
+  user_data: schemas.SimplePayCreate,
+  lang: str=''
+):
+    print(user_data)
+    numbers = string.digits
+    randomNumber = ''.join(choice(numbers) for _ in range(4))
+    MerchantTradeNo = datetime.now().strftime("%Y%m%d%H%M%SNO")+randomNumber
+    remark = {'MerchantTradeNo':MerchantTradeNo}
+    remark_string = json.dumps(remark, ensure_ascii=False)
+    simplepay = crud.simplepay.create_with_payment_data(db, 
+                                                obj_in=user_data, 
+                                                epayment='ecpay',
+                                                remark=remark_string)
+    order_params = {
+        'MerchantTradeNo': MerchantTradeNo,
+        'StoreID': 'SaaS',
+        'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
+        'PaymentType': 'aio',
+        'TotalAmount': user_data.amount,
+        'TradeDesc': user_data.tradeDesc,
+        'ItemName': user_data.item,
+        'ReturnURL': 'https://cloud.choozmo.com/api/v1/payment/simple-pay-ecpay-result-return',
+        'ChoosePayment': 'ALL',
+        'ClientBackURL': 'https://cloud.choozmo.com/simple-pay',
+        'ItemURL': 'https://cloud.choozmo.com/simple-pay',
+        'Remark': '',
+        'ChooseSubPayment': '',
+        'OrderResultURL': '',
+        'NeedExtraPaidInfo': 'Y',
+        'DeviceSource': '',
+        'IgnorePayment': 'ATM#CVS#BARCODE',
+        'PlatformID': '',
+        'InvoiceMark': 'N',
+        'CustomField1': str(simplepay.id),
+        'CustomField2': user_data.taxID if user_data.taxID else '',
+        'CustomField3': '',
+        'CustomField4': '',
+        'EncryptType': 1,
+        'Language': lang,
+    }
+
+    extend_params_1 = {
+        'ExpireDate': 3,
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_2 = {
+        'StoreExpireDate': 7,
+        'Desc_1': '',
+        'Desc_2': '',
+        'Desc_3': '',
+        'Desc_4': '',
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_3 = {
+        'BindingCard': 0,
+        'MerchantMemberID': '',
+    }
+
+    extend_params_4 = {
+        'Redeem': 'N',
+        'UnionPay': 0,
+    }
+
+    inv_params = {
+        'RelateNumber': MerchantTradeNo, # 特店自訂編號
+        # 'CustomerID': 'TEA_0000001', # 客戶編號
+        'CustomerIdentifier': user_data.taxID if user_data.taxID else '', # 統一編號
+        'CustomerName': user_data.name,
+        # 'CustomerAddr': '客戶地址',
+        # 'CustomerPhone': '0912345678', # 客戶手機號碼
+        'CustomerEmail': user_data.email,
+        # 'ClearanceMark': '2', # 通關方式
+        # 'TaxType': '1', # 課稅類別
+        # 'CarruerType': '', # 載具類別
+        # 'CarruerNum': '', # 載具編號
+        # 'Donation': '1', # 捐贈註記
+        # 'LoveCode': '168001', # 捐贈碼
+        # 'Print': '1',
+        # 'InvoiceItemName': '測試商品1|測試商品2',
+        # 'InvoiceItemCount': '2|3',
+        # 'InvoiceItemWord': '個|包',
+        # 'InvoiceItemPrice': '35|10',
+        # 'InvoiceItemTaxType': '1|1',
+        # 'InvoiceRemark': '測試商品1的說明|測試商品2的說明',
+        # 'DelayDay': '0', # 延遲天數
+        # 'InvType': '07', # 字軌類別
+    }
+
+    # 建立實體
+    ecpay_payment_sdk = ECPayPaymentSdk(
+        MerchantID='3226141',
+        HashKey='OhcjDTeXK9PKW9vb',
+        HashIV='AfOmUM06S0bt8KPE'
+    )
+
+    # 合併延伸參數
+    order_params.update(extend_params_1)
+    order_params.update(extend_params_2)
+    order_params.update(extend_params_3)
+    order_params.update(extend_params_4)
+
+    # 合併發票參數
+    order_params.update(inv_params)
+    try:
+        # 產生綠界訂單所需參數
+        final_order_params = ecpay_payment_sdk.create_order(order_params)
+
+        # 產生 html 的 form 格式
+        # action_url = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'  # 測試環境
+        action_url = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5' # 正式環境
+        html = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
+
+        return html
+    except Exception as error:
+        print('An exception happened: ' + str(error))
+
+
+@router.get('/simpleypay-list-all', response_model=List[schemas.SimplePay])
+def get_list(
+  *,
+  db: Session = Depends(deps.get_db),
+):
+  simplepay_list = crud.simplepay.get_multi(db)
+  return 
+
+@router.post("/simple-ecpay-result-return")
+def ecpay_return(
+    *,
+    db: Session = Depends(deps.get_db),
+    MerchantID: Optional[str]=Form(None),
+    MerchantTradeNo: Optional[str]=Form(None),
+    StoreID: Optional[str]=Form(None),
+    RtnCode: Optional[int]=Form(None),
+    RtnMsg: Optional[str]=Form(None),
+    TradeNo: Optional[str]=Form(None),
+    TradeAmt: Optional[int]=Form(None),
+    PaymentDate: Optional[str]=Form(None),
+    PaymentType: Optional[str]=Form(None),
+    PaymentTypeChargeFee: Optional[int]=Form(None),
+    TradeDate: Optional[str]=Form(None),
+    SimulatePaid: Optional[int]=Form(None),
+    CustomField1: Optional[str]=Form(None),
+    CustomField2: Optional[str]=Form(None),
+    CustomField3: Optional[str]=Form(None),
+    CustomField4: Optional[str]=Form(None),
+    CheckMacValue: Optional[str]=Form(None),
+) -> Any:
+  #送email
+  print(f"\
+MerchantID: {MerchantID} \n\
+MerchantTradeNo: {MerchantTradeNo}\n\
+StoreID: {StoreID}\n\
+RtnCode: {RtnCode}\n\
+RtnMsg: {RtnMsg}\n\
+TradeNo: {TradeNo}\n\
+TradeAmt: {TradeAmt}\n\
+PaymentDate: {PaymentDate}\n\
+PaymentType: {PaymentType}\n\
+PaymentTypeChargeFee: {PaymentTypeChargeFee}\n\
+TradeDate: {TradeDate}\n\
+SimulatePaid: {SimulatePaid}\n\
+CustomField1: {CustomField1}\n\
+CustomField2: {CustomField2}\n\
+CustomField3: {CustomField3}\n\
+CustomField4: {CustomField4}\n\
+CheckMacValue: {CheckMacValue}\
+  ")
+  
+  if RtnCode==1:
+    simplepay_id = int(CustomField1)
+    simplepay = crud.simplepay.get(db=db, id=simplepay_id)
+    remark = json.loads(simplepay.remark)
+    remark['TradeNo'] = TradeNo
+    crud.simplepay.update(db, db_obj=simplepay, obj_in={"payment_state":"succeeded", "remark":json.dumps(remark, ensure_ascii=False)})
+  return Response(content='1', status_code=status.HTTP_200_OK)

+ 371 - 0
backend/app/app/api/api_v1/endpoints/text2zip.py

@@ -0,0 +1,371 @@
+from typing import Any, List, Optional, Literal
+from datetime import datetime
+from fastapi import APIRouter, Body, Depends, HTTPException, Form, status, Response, BackgroundTasks
+from fastapi.encoders import jsonable_encoder
+from pydantic.networks import EmailStr, HttpUrl
+from sqlalchemy.orm import Session
+from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
+import app.crud as crud
+import app.models as models
+import app.schemas as schemas 
+from app.api import deps
+from app.core.config import settings
+from app.core.ecpay_payment_sdk import ECPayPaymentSdk
+from app.utils import send_new_account_email
+from pydantic import BaseModel
+import requests
+from random import choice
+import string
+import json
+import os
+import PIL.Image
+from gradio_client import Client
+from openai import OpenAI
+import webuiapi
+import datetime
+from pathlib import Path
+import openpyxl as px
+import tempfile
+import shutil
+import PIL
+import re
+from app.db.session import SessionLocal
+import requests
+import asyncio
+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
+import emails
+from fastapi import UploadFile, File, Form
+from app.core.video_utils import update_zip
+
+BACKEND_ZIP_STORAGE = Path("/app").joinpath(settings.BACKEND_ZIP_STORAGE)
+LOCAL_ZIP_STORAGE = Path("/").joinpath(settings.LOCAL_ZIP_STORAGE)
+
+LINE_URL = 'https://notify-api.line.me/api/notify'
+LINE_TOKEN = 'o8dqdVL2k8aiWO4jy3pawZamBu53bbjoSh2u0GJ7F0j'
+
+router = APIRouter()
+
+def gen_prompt(content:str):
+    client = OpenAI(api_key='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')
+
+    completion = client.chat.completions.create(
+        model="gpt-4o-mini",
+        messages=[
+            {"role": "assistant", "content": "You are a helpful image genaration prompt engineer. \
+             You will convert the following inputs into English prompts for image generation AI and respond accordingly. \
+             Do not start with 'Create an image. \
+             Keep it within 50 words"},
+            {
+                "role": "user",
+                "content": content
+            }
+        ]
+    )
+    return completion.choices[0].message.content
+
+def gen_flux_image(prompt):
+    client = Client("http://192.168.192.83:7860/")
+    result = client.predict(
+        model_id="models/FLUX.1-schnell",
+        prompt=prompt,
+        width=1280,
+        height=720,
+        seed=-1,
+        steps=4,
+        guidance_scale=3.5,
+        add_sampling_metadata=True,
+        api_name="/generate"
+    )
+    with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as t:
+        PIL.Image.open(result[0]).convert("RGB").save(t.name,"jpeg")
+        return t.name
+
+def gen_sd_image(prompt):
+    api = webuiapi.WebUIApi(host='192.168.192.38', port=7860)
+    api.util_set_model('sd3_medium')
+    result = api.txt2img(prompt=prompt,
+                    negative_prompt="",
+                    seed=-1,
+                    cfg_scale=4,
+                    sampler_name='Euler',
+                    scheduler='Automatic',
+                    steps=40,
+                    width=1280,
+                    height=720,
+                    )
+    with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as t:
+        result.image.save(t, "jpeg")
+    return t.name
+
+punctuation = r"[.,!?;:。 、!?,;:]"
+
+
+
+@router.post('/gen-zip')
+async def generate_zip(
+      *,
+    background_tasks: BackgroundTasks,
+    current_user: models.User = Depends(deps.get_current_active_user),
+    
+    upload_file: UploadFile=File(),
+    texts:List[str]):
+
+    if not model:
+        model = 'sd3'
+    wb = px.Workbook()
+    ws = wb.active
+    ws.title = 'script'
+    ws['A1'] = '大標'
+    ws['B1'] = '字幕'
+    ws['C1'] = '素材'
+    with tempfile.TemporaryDirectory() as td:
+        dir = Path(f'{td}/{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}')
+        dir.mkdir(exist_ok=False)
+        texts = [text for text in texts if text]
+        for i, text in enumerate(texts):
+            print(f'{i+1}/{len(texts)}')
+            prompt = gen_prompt(text)
+            if model=='flux':
+                img_path = Path(gen_flux_image(prompt))
+
+            elif model=='sd3':
+                img_path = Path(gen_sd_image(prompt))
+            print("before", str(img_path))
+            img_path = img_path.rename(dir/(f'{i+1:02}'+img_path.suffix))
+            print("after", str(img_path))
+            ws['B'+ str(i+2)] = re.sub(punctuation, r"\\", text)
+            ws['C'+ str(i+2)] = img_path.name
+        excel_path = Path(dir/'script.xlsx')
+        wb.save(excel_path)
+        output_dir = '/tmp'
+        shutil.make_archive(f'{output_dir}/{dir.name}', format='zip', root_dir=td)
+        def remove_zip():
+            if os.path.exists(f'{output_dir}/{dir.name}.zip'):
+                os.remove(f'{output_dir}/{dir.name}.zip')
+        background_tasks.add_task(remove_zip)
+    return FileResponse(f'{output_dir}/{dir.name}.zip', media_type="application/zip")
+
+@router.post('/gen-video')
+async def generate_video(
+      *,
+    background_tasks: BackgroundTasks,
+    #current_user: models.User = Depends(deps.get_current_active_user),
+    model:Literal['sd3', 'flux']="sd3", 
+    email:EmailStr,
+    texts:List[str],
+    lang:Literal["en", 'zh']):
+    
+    flag=False
+    for text in texts:
+        if text:
+            flag=True
+    if flag:
+        background_tasks.add_task(wait_finish, model, email, texts, lang)
+        return PlainTextResponse("OK")
+    else:
+        return HTTPException("No texts.")
+
+async def wait_finish(model, email, texts, lang): 
+    await asyncio.sleep(1)
+    if not model:
+        model = 'sd3'
+    wb = px.Workbook()
+    ws = wb.active
+    ws.title = 'script'
+    ws['A1'] = '大標'
+    ws['B1'] = '字幕'
+    ws['C1'] = '素材'
+    td = Path(f'/tmp/{datetime.datetime.now().strftime("%Y%m%d%H%M%S-td")}')
+    td.mkdir(exist_ok=False)
+    dir = td/f'{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}'
+    dir.mkdir(exist_ok=False)
+    texts = [text for text in texts if text]
+    for i, text in enumerate(texts):
+        print(f'{i+1}/{len(texts)}')
+        prompt = gen_prompt(text)
+        if model=='flux':
+            img_path = Path(gen_flux_image(prompt))
+
+        elif model=='sd3':
+            img_path = Path(gen_sd_image(prompt))
+        print("before", str(img_path))
+        img_path = img_path.rename(dir/(f'{i+1:02}'+img_path.suffix))
+        print("after", str(img_path))
+        ws['B'+ str(i+2)] = re.sub(punctuation, r"\\", text)
+        ws['C'+ str(i+2)] = img_path.name
+    excel_path = Path(dir/'script.xlsx')
+    wb.save(excel_path)
+    output_dir = '/tmp'
+    shutil.make_archive(f'{output_dir}/{dir.name}', format='zip', root_dir=td)
+    # def remove_zip():
+    #     if os.path.exists(f'{output_dir}/{dir.name}.zip'):
+    #         os.remove(f'{output_dir}/{dir.name}.zip')
+    db = SessionLocal()
+    current_user = crud.user.get(db, id=0)
+    video_create = schemas.VideoCreate(title="guest", progress_state="PENDING", stored_filename=dir.name)
+    video = crud.video.create_with_owner(db=db, obj_in=video_create, owner_id=current_user.id)
+    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']
+    video_data['character'] = "hannah-2"
+    video_data['anchor'] = "hannah-2"
+    video_data['style'] = "style14"
+    video_data['lang'] = lang
+    video_data['email'] = email
+    db.close()
+    
+    zip_filename = video_data['stored_filename']+".zip"
+    process = await asyncio.create_subprocess_exec("sshpass", "-p", "choozmo9", 
+                    "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", f"/tmp/{zip_filename}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE)}")
+    
+    await process.wait()
+    if os.path.exists(f"/tmp/{zip_filename}"):
+        os.remove(f"/tmp/{zip_filename}")
+    if td.exists():
+        shutil.rmtree(str(td))
+    headers = {
+        'Authorization': 'Bearer ' + LINE_TOKEN    
+    }
+    data = {
+        'message':f'cloud-choozmo-com\n \
+                    Video\n \
+                    user: {current_user.id}\n \
+                    video: {video_data["id"]}\n \
+                    state: queuing'     
+    }
+    data = requests.post(LINE_URL, headers=headers, data=data) 
+    task = celery_app.send_task("app.worker.make_video", kwargs=video_data)
+    while True:
+       await asyncio.sleep(1)
+       if task.state != "PENDING":
+           break
+       
+    db = SessionLocal()
+    video = db.query(models.Video).get(video_data['id'])
+    video.progress_state = "STARTED" 
+    db.commit()
+    db.close()
+    msg_data = f"{video_data['stored_filename']}:STARTED"
+    headers = {
+        'Authorization': 'Bearer ' + LINE_TOKEN    
+    }
+    data = {
+            'message':f'cloud-choozmo-com/gen-video\n \
+                        Video\n \
+                        guest user: {email}\n \
+                        video: {video_data["id"]}\n \
+                    state: start'     
+    }
+    data = requests.post(LINE_URL, headers=headers, data=data) 
+    while True:
+        await asyncio.sleep(1)
+        if task.state != "STARTED":
+            break
+
+    if task.state == "SUCCESS":
+        db = SessionLocal()
+        video = db.query(models.Video).get(video_data['id'])
+        user = db.query(models.User).get(video_data['owner_id'])
+        video.progress_state = "SUCCESS" 
+        if time := task.result:
+            user.available_time -= int(time) if int(time) <= user.available_time else 0
+            video.length = int(time)
+        db.commit()
+        db.close()
+        msg_data = f"{video_data['stored_filename']}:SUCCESS:{int(time)}"
+        headers = {
+            'Authorization': 'Bearer ' + LINE_TOKEN    
+        }
+        data = {
+            'message':f'cloud-choozmo-com/gen-video\n \
+                        Video\n \
+                        guest user: {email}\n \
+                        video: {video_data["id"]}\n \
+                        state: success'    
+        }
+        data = requests.post(LINE_URL, headers=headers, data=data) 
+        send_simple_email(email_to=video_data['email'], video_path=f"http://172.104.93.163:30080/{video_data['stored_filename']}.mp4")
+
+    elif task.state == "FAILURE":
+        db = SessionLocal()
+        video = db.query(models.Video).get(video_data['id'])
+        video.progress_state = "FAILURE" 
+        db.commit()
+        db.close()
+        msg_data = f"{video_data['stored_filename']}:FAILURE"
+
+        headers = {
+            'Authorization': 'Bearer ' + LINE_TOKEN   
+        }
+        data = {
+            'message':f'cloud-choozmo-com/gen-video\n \
+                        Video\n \
+                        guest user: {email}\n \
+                        video: {video_data["id"]}\n \
+                        state: failure'     
+        }
+        data = requests.post(LINE_URL, headers=headers, data=data) 
+        send_simple_email(email_to=video_data['email'], video_path=None)
+        
+def send_simple_email(email_to: str, video_path: str):
+    if video_path==None:
+        message = emails.html(html="<p>Sorry!<br>The video failed to produce for some reason.</p>",
+                          subject="Choozmo Video Service",
+                          mail_from=('ChoozMo Video Service', 'verify@choozmo.com'))
+        r = message.send(to=email_to, mail_from='verify@choozmo.com', smtp={'host': 'smtp.gmail.com',
+                                                                        'port': '587',
+                                                                        'tls':True,
+                                                                        'user':'verify@choozmo.com',
+                                                                        'password':'hlmaxzjnvpeaulhw',
+                                                                        'timeout': 30})
+        print(r)
+        assert r.status_code == 250
+    else:
+        message = emails.html(html=f"<p>Hi!<br>Here is <a href='{video_path}'>Video</a></p>",
+                            subject="Choozmo Video Service",
+                            mail_from=('ChoozMo Video Service', 'verify@choozmo.com'))
+        r = message.send(to=email_to, mail_from='verify@choozmo.com', smtp={'host': 'smtp.gmail.com',
+                                                                            'port': '587',
+                                                                            'tls':True,
+                                                                            'user':'verify@choozmo.com',
+                                                                            'password':'hlmaxzjnvpeaulhw',
+                                                                            'timeout': 30})
+        print(r)
+        assert r.status_code == 250
+        
+@router.post('/zip-translate')
+def zip_translate(
+    *,
+    background_tasks: BackgroundTasks,
+    upload_file: UploadFile=File(),
+    lang:str):
+
+    try:
+        with open(upload_file.filename, 'wb') as f:
+            while contents := upload_file.file.read(1024 * 1024):
+                f.write(contents)
+    except Exception as e:
+        print(e, type(e))
+        error_msg = {"error_message": str(e)}
+        return JSONResponse(error_msg)
+    finally:
+        upload_file.file.close()
+    try:
+      if check_zip(upload_file.filename):
+        print("passed check_zip")
+    except VideoMakerError as e:
+      print(e)
+      error_msg = {"accepted": False, "error_message":f'{e}'}
+      return JSONResponse(error_msg)
+    path = Path(upload_file.filename)
+    update_zip(str(path), 'zh-TW', str(path.parent/(path.stem+"_"+lang+path.suffix)))
+
+    def remove_zip():
+            if os.path.exists(str(path.parent/(path.stem+"_"+lang+path.suffix))):
+                os.remove(str(path.parent/(path.stem+"_"+lang+path.suffix)))
+    background_tasks.add_task(remove_zip)
+    return FileResponse(str(path.parent/(path.stem+"_"+lang+path.suffix)), media_type="application/zip")

+ 0 - 17
backend/app/app/core/test_chardet.py

@@ -1,17 +0,0 @@
-from chardet.universaldetector import UniversalDetector
-
-DEFAULT_ENCODING = "utf-8"
-
-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
-  
-if __name__=="__main__":
-  pass

+ 58 - 1
backend/app/app/core/video_utils.py

@@ -5,8 +5,22 @@ import shutil
 import os
 import chardet
 import zipfile
-from test_chardet import guess_codec
 from io import BytesIO
+from translate import Translator
+from chardet.universaldetector import UniversalDetector
+
+DEFAULT_ENCODING = "utf-8"
+
+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)
@@ -54,5 +68,48 @@ def check_zip(zip_filepath:str):
         n = stems.count(voice_file)
         if n != 1:
           raise ValueError(f"voice file is can't find is zip at scene {i+1}.")
+      
+def update_zip(zip_path, lang):
+    temp_zip_path = zip_path + ".tmp"
+
+    with zipfile.ZipFile(zip_path, 'r') as zip_in, zipfile.ZipFile(temp_zip_path, 'w') as zip_out:
+        for item in zip_in.infolist():
+            with zip_in.open(item.filename) as src_file:
+                if item.filename.split('.')[-1] == "xlsx":
+                    table = pd.read_excel(src_file, dtype=object)
+                    table = translate_table(table, lang)
+                    table.to_excel(Path(item.filename).name ,sheet_name='Sheet_name_1')
+                    zip_out.write(Path(item.filename).name, item.filename)
+                    os.remove(Path(item.filename).name)
+                elif item.filename.split('.')[-1] == "csv":
+                    table = pd.read_csv(src_file, dtype=object)
+                    table = translate_table(table, lang)
+                    table.to_excel(Path(item.filename).name ,sheet_name='Sheet_name_1')
+                    zip_out.write(Path(item.filename).name, item.filename)
+                    os.remove(Path(item.filename).name)
+                else:
+                    # それ以外のファイルはそのままコピー
+                    with zip_out.open(item.filename, 'w') as dst_file:
+                        shutil.copyfileobj(src_file, dst_file)
+
+    # 旧ZIPを削除し、新ZIPをリネーム
+    os.remove(zip_path)
+    os.rename(temp_zip_path, zip_path)
+    
+def translate_table(table, lang):
+    translator= Translator(to_lang=lang)
+    print(f"translate to {lang}")
+    for i in range(len(table)):
+        if (text:=table.loc[i, ['大標']].item()):
+            print("大標:",text)
+            translation = translator.translate(text)
+            print("大標翻譯:",translation)
+            table.loc[i, ['字幕']] = translation
+        if (text:=table.loc[i, ['字幕']].item()):
+            print('字幕:',text)
+            translation = translator.translate(text)
+            print('字幕翻譯:',translation)
+            table.loc[i, ['字幕']] = translation
+    return table
         
     

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

@@ -4,6 +4,7 @@ from .crud_article import article
 from .crud_ser_no import serial_number
 from .crud_ytviews import ytviews
 from .crud_payment import payment
+from .crud_simplepay import simplepay
 # For a new basic set of CRUD operations 
 # you could just do
 

+ 35 - 0
backend/app/app/crud/crud_simplepay.py

@@ -0,0 +1,35 @@
+from typing import List
+
+from fastapi.encoders import jsonable_encoder
+from sqlalchemy.orm import Session
+from sqlalchemy import desc
+from app.crud.base import CRUDBase
+from app.models.simplepay import SimplePay
+from app.schemas.simplepay import SimplePayCreate, SimplePayUpdate
+
+from app.utils import random_name
+
+class CRUDSimplePay(CRUDBase[SimplePay, SimplePayCreate, SimplePayUpdate]):
+    def create_with_payment_data(
+        self, db: Session, *, obj_in: SimplePayCreate, 
+        epayment:str,
+        remark: str
+    ) -> SimplePay:
+        
+        obj_in_data = jsonable_encoder(obj_in)
+        db_obj = self.model(**obj_in_data, 
+                            epayment=epayment,
+                            remark=remark)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+      
+    def get_all_desc(self, db: Session) -> List[SimplePay]:
+        return (
+            db.query(self.model)
+            .order_by(desc(SimplePay.id))
+            .all()
+        )
+        
+simplepay = CRUDSimplePay(SimplePay)

+ 4 - 1
backend/app/app/db/base.py

@@ -9,4 +9,7 @@ from app.models.article import Article
 from app.models.serial_number import SerialNumber
 from app.models.voice import Voice
 from app.models.payment import Payment
-from app.models.ytviews import YTViews
+from app.models.ytviews import YTViews
+from app.models.simplepay import SimplePay
+#from app.models.bind_character import BindCharacter
+#from app.models.bind_frame import BindFrame

+ 4 - 1
backend/app/app/models/__init__.py

@@ -5,4 +5,7 @@ from .article import Article
 from .serial_number import SerialNumber
 from .voice import Voice
 from .payment import Payment
-from .ytviews import YTViews
+from .ytviews import YTViews
+from .simplepay import SimplePay
+#from .bind_character import BindCharacter
+#from .bind_frame import BindFrame

+ 27 - 0
backend/app/app/models/simplepay.py

@@ -0,0 +1,27 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String, Enum, DateTime, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.db.base_class import Base
+
+
+if TYPE_CHECKING:
+  from .user import User  # noqa: F401
+  from .enum import Progress
+
+class SimplePay(Base):
+  id = Column(Integer, primary_key=True, index=True)
+  email = Column(String(50), index=True, nullable=False)
+  name = Column(String(30), index=True)
+  taxID = Column(String(20))
+  tradeDesc = Column(String(20))
+  item = Column(String(20), nullable=False)
+  amount = Column(Integer, nullable=False)
+  created_datetime = Column(DateTime(timezone=True), default=func.now())
+  payment_state = Column(String(10), default='waiting', nullable=False)
+  epayment = Column(String(10), 
+                    ForeignKey("epayment.name", ondelete="RESTRICT", onupdate="CASCADE"),
+                    default="ecpay")
+  remark = Column(Text)
+  

+ 2 - 1
backend/app/app/schemas/__init__.py

@@ -5,4 +5,5 @@ from .video import Video, VideoCreate, VideoInDB, VideoUpdate
 from .article import ArticleBase, ArticleCreate, ArticleUpdate
 from .serial_number import SerialNumberBase, SerialNumberCreate, SerialNumberUpdate
 from .ytviews import YTViewsBase, YTViewsCreate, YTViews
-from .payment import PaymentBase, PaymentCreate, PaymentInDB, PaymentUpdate
+from .payment import PaymentBase, PaymentCreate, PaymentInDB, PaymentUpdate
+from .simplepay import SimplePay, SimplePayCreate, SimplePayInDB, SimplePayUpdate

+ 48 - 0
backend/app/app/schemas/simplepay.py

@@ -0,0 +1,48 @@
+from typing import Optional
+
+from pydantic import BaseModel, EmailStr, HttpUrl
+
+# Shared properties
+class SimplePayBase(BaseModel):
+    email: Optional[EmailStr] = None
+    name : Optional[str] = None
+    taxID: Optional[str] = None
+    tradeDesc: Optional[str] = None
+    item: Optional[str] = None
+    amount: Optional[int] = None
+    
+# Properties to receive on video creation
+class SimplePayCreate(SimplePayBase):
+    email: EmailStr
+    name : str
+    tradeDesc: Optional[str] = None
+    item: Optional[str] = None
+    amount: Optional[int] = None
+    
+# Properties to receive on video update
+class SimplePayUpdate(SimplePayBase):
+    pass
+
+# Properties shared by models stored in DB
+class SimplePayInDBBase(SimplePayBase):
+    id: int
+    email: EmailStr
+    name : str
+    taxID: str
+    tradeDesc: str
+    item: str
+    amount: int
+    payment_state: str
+    
+    class Config:
+        orm_mode = True
+
+
+# Properties to return to client
+class SimplePay(SimplePayInDBBase):
+    pass
+
+
+# Properties properties stored in DB
+class SimplePayInDB(SimplePayInDBBase):
+    pass

+ 1 - 9
backend/app/prestart.sh

@@ -7,12 +7,4 @@ python /app/app/backend_pre_start.py
 # alembic upgrade head
 
 # Create initial data in DB
-python /app/app/initial_data.py
-
-if [ ! -e BACKEND_ZIP_STORAGE ]; then
-    mkdir BACKEND_ZIP_STORAGE
-fi
-
-if [ ! -e BACKEND_VIDEOS_STORAGE ]; then
-    mkdir BACKEND_VIDEOS_STORAGE
-fi
+python /app/app/initial_data.py

+ 4 - 1
backend/app/pyproject.toml

@@ -31,7 +31,10 @@ python-jose = {extras = ["cryptography"], version = "^3.3.0"}
 pandas = "^2.0.0"
 openpyxl = "^3.1.0"
 chardet = "^5.1.0"
-gradio_client = "^1.3.0"
+openai = "^1.59.4"
+gradio_client = "^1.5.3"
+webuiapi = "^0.9.17"
+translate = "^3.6.1"
 
 [tool.poetry.dev-dependencies]
 mypy = "^0.991"

+ 2 - 0
see_backend_log.sh

@@ -0,0 +1,2 @@
+#!/bin/bash
+docker service logs ai-anchor-com_backend