├── daos ├── __init__.py └── user_dao.py ├── models ├── __init__.py └── user.py ├── controllers ├── __init__.py ├── user_controller.py └── line_bot_controller.py ├── services ├── __init__.py ├── audio_service.py ├── video_service.py ├── user_service.py └── image_service.py ├── converted_savedmodel ├── labels.txt └── model.savedmodel │ ├── saved_model.pb │ └── variables │ ├── variables.index │ └── variables.data-00000-of-00001 ├── .gitignore ├── line_message_json ├── 李秉鴻.json └── 大話AWS.json ├── requirements.txt ├── Dockerfile ├── README.md ├── docker-compose.yml ├── utils └── reply_send_message.py └── app.py /daos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /converted_savedmodel/labels.txt: -------------------------------------------------------------------------------- 1 | 0 李秉鴻 2 | 1 大話AWS 3 | 2 其他 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__ 2 | .idea 3 | ngrok 4 | venv 5 | ngrok 6 | -------------------------------------------------------------------------------- /line_message_json/李秉鴻.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "text", 4 | "text": "秉鴻" 5 | } 6 | ] -------------------------------------------------------------------------------- /line_message_json/大話AWS.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "text", 4 | "text": "只是測試用" 5 | } 6 | ] -------------------------------------------------------------------------------- /converted_savedmodel/model.savedmodel/saved_model.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BingHongLi/ncu_gcp_ai_project/HEAD/converted_savedmodel/model.savedmodel/saved_model.pb -------------------------------------------------------------------------------- /converted_savedmodel/model.savedmodel/variables/variables.index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BingHongLi/ncu_gcp_ai_project/HEAD/converted_savedmodel/model.savedmodel/variables/variables.index -------------------------------------------------------------------------------- /converted_savedmodel/model.savedmodel/variables/variables.data-00000-of-00001: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BingHongLi/ncu_gcp_ai_project/HEAD/converted_savedmodel/model.savedmodel/variables/variables.data-00000-of-00001 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | line-bot-sdk 3 | wheel 4 | grpcio 5 | google-cloud-firestore 6 | flask_cors 7 | line-bot-sdk 8 | google-cloud-storage 9 | pandas 10 | gunicorn 11 | google-cloud-logging 12 | tensorflow-cpu == 2.4.1 13 | Pillow -------------------------------------------------------------------------------- /controllers/user_controller.py: -------------------------------------------------------------------------------- 1 | from services.user_service import UserService 2 | from flask import Request,Response 3 | import json 4 | 5 | class UserController: 6 | 7 | @classmethod 8 | def get_user(cls,request:Request): 9 | user_id= request.args.get('line_user_id') 10 | user = UserService.get_user(user_id) 11 | return user.to_dict() 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official lightweight Python image. 2 | # https://hub.docker.com/_/python 3 | FROM python:3.7-slim 4 | 5 | # Allow statements and log messages to immediately appear in the Knative logs 6 | ENV PYTHONUNBUFFERED True 7 | 8 | # Copy local code to the container image. 9 | ENV APP_HOME /app 10 | WORKDIR $APP_HOME 11 | COPY . ./ 12 | 13 | # Install production dependencies. 14 | RUN pip install -r requirements.txt 15 | 16 | # Run the web service on container startup. Here we use the gunicorn 17 | # webserver, with one worker process and 8 threads. 18 | # For environments with multiple CPU cores, increase the number of workers 19 | # to be equal to the cores available. 20 | CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GCP帳號申辦與建置GCP Project 2 | 3 | 4 | # 環境準備 5 | 6 | 建置 Project 7 | 8 | 建置 cloud storage 9 | 10 | 建置 firestore 11 | 12 | # 程式碼準備與部署 13 | 14 | 複製下列網址,並開啟。 15 | 16 | https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FBingHongLi%2Fncu_gcp_ai_project&cloudshell_open_in_editor=README.md&cloudshell_workspace=. 17 | 18 | # 指定教材資料夾為工作目錄 19 | 20 | # 將訓練好的模型放入 converted_savedmodel資料夾 21 | 22 | 23 | # 將要回應的json放入line_message_json資料夾 24 | 25 | 26 | # 構建程式碼 27 | 28 | ``` 29 | gcloud config set project YOUR-PROJECT-ID 30 | gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/ncu-bot-dev:0.0.1 31 | ``` 32 | 33 | # 部署 34 | 35 | 指定環境變數 36 | 37 | ``` 38 | USER_INFO_GS_BUCKET_NAME: 剛剛建立的資料桶子 39 | LINE_CHANNEL_ACCESS_TOKEN: 課程內述說 40 | LINE_CHANNEL_SECRET: 課程內述說 41 | ``` 42 | # 將生成的網址追加callback 貼回line網站的webhook 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pgadmin4: 3 | container_name: pgadmin4 4 | image: dpage/pgadmin4 5 | restart: always 6 | environment: 7 | PGADMIN_DEFAULT_EMAIL: "ncu@ncu.edu.tw" 8 | PGADMIN_DEFAULT_PASSWORD: "ncu+123" 9 | ports: 10 | - "5050:80" 11 | depends_on: 12 | - ncu-postgre 13 | 14 | 15 | postgrest: 16 | image: postgrest/postgrest 17 | ports: 18 | - "3000:3000" 19 | environment: 20 | PGRST_DB_URI: postgres://ncucc:ncuccpw@ncu-postgre:5432/ncu 21 | PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000 22 | PGRST_DB_ANON_ROLE: web_anon 23 | PGRST_DB_SCHEMA: public 24 | depends_on: 25 | - ncu-postgre 26 | 27 | 28 | ncu-postgre: 29 | image: pgvector/pgvector:0.7.4-pg17 30 | #image: postgis/postgis:13-master 31 | # Required when running on platform other than amd64, like Apple M1/M2: 32 | # platform: linux/amd64 33 | volumes: 34 | - ./data/ncu-database:/var/lib/postgresql/data 35 | environment: 36 | POSTGRES_USER: "ncucc" 37 | POSTGRES_PASSWORD: "ncuccpw" 38 | POSTGRES_DB: "ncu" 39 | healthcheck: 40 | test: ["CMD", "pg_isready", "--host=localhost", "--username=ncucc"] 41 | interval: 10s 42 | timeout: 5s 43 | retries: 5 44 | start_interval: 5s 45 | start_period: 30s 46 | -------------------------------------------------------------------------------- /daos/user_dao.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 負責與db溝通 3 | save_user(user:User) :新增資料時,若有重複資料,則採更新 4 | get_user(user_id:str):取用資料,開放以user_id的方式尋找 5 | ''' 6 | 7 | from models.user import User 8 | from google.cloud import firestore 9 | 10 | 11 | class UserDAO: 12 | # 建立客戶端 13 | db = firestore.Client() 14 | users_ref = db.collection(u'users') 15 | 16 | # 新增資料時,若有重複資料,則採更新 17 | @classmethod 18 | def save_user(cls, user: User) -> None: 19 | # print(user) 20 | 21 | # 查找用戶資料 22 | user_ref = cls.users_ref.document(user.line_user_id) 23 | user_doc = user_ref.get() 24 | 25 | # 檢查用戶資料是否存在 26 | if user_doc.exists: 27 | # 更新 28 | user_ref.set(document_data=user.to_dict(), merge=True) 29 | # print(u'has already update') 30 | else: 31 | # 直接插入 32 | cls.users_ref.add(document_data=user.to_dict(), document_id=user.line_user_id) 33 | # print(u'No such document!') 34 | return user.to_dict() 35 | 36 | # 取用資料,開放以user_id的方式尋找 37 | @classmethod 38 | def get_user(cls, user_id: str) -> User: 39 | 40 | # 設定要查找的資料 41 | user_ref = cls.users_ref.document(user_id) 42 | 43 | # 查找資料 44 | user_doc = user_ref.get() 45 | 46 | # 若資料存在 47 | if user_doc.exists: 48 | user = User.from_dict(user_doc.to_dict()) 49 | # print(f'Document data: {user_doc.to_dict()}') 50 | else: 51 | pass 52 | # print(u'No such document!') 53 | 54 | return user -------------------------------------------------------------------------------- /services/audio_service.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | 用戶上傳照片時,將照片從Line取回,放入CloudStorage 4 | 5 | 瀏覽用戶目前擁有多少張照片(未) 6 | 7 | ''' 8 | 9 | from models.user import User 10 | from flask import Request 11 | from linebot import ( 12 | LineBotApi 13 | ) 14 | 15 | import os 16 | from daos.user_dao import UserDAO 17 | from linebot.models import ( 18 | TextSendMessage 19 | ) 20 | 21 | 22 | # 圖片下載與上傳專用 23 | import urllib.request 24 | from google.cloud import storage 25 | 26 | 27 | class AudioService: 28 | line_bot_api = LineBotApi(channel_access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"]) 29 | 30 | ''' 31 | 用戶上傳照片 32 | 將照片取回 33 | 將照片存入CloudStorage內 34 | ''' 35 | @classmethod 36 | def line_user_upload_video(cls,event): 37 | 38 | # 取出照片 39 | image_blob = cls.line_bot_api.get_message_content(event.message.id) 40 | temp_file_path=f"""{event.message.id}.mp3""" 41 | 42 | # 43 | with open(temp_file_path, 'wb') as fd: 44 | for chunk in image_blob.iter_content(): 45 | fd.write(chunk) 46 | 47 | # 上傳至bucket 48 | storage_client = storage.Client() 49 | bucket_name = os.environ['USER_INFO_GS_BUCKET_NAME'] 50 | destination_blob_name = f'{event.source.user_id}/audio/{event.message.id}.mp3' 51 | bucket = storage_client.bucket(bucket_name) 52 | blob = bucket.blob(destination_blob_name) 53 | blob.upload_from_filename(temp_file_path) 54 | 55 | # 移除本地檔案 56 | os.remove(temp_file_path) 57 | 58 | # 回覆消息 59 | cls.line_bot_api.reply_message( 60 | event.reply_token, 61 | TextSendMessage(f"""音訊已上傳,請期待未來的AI服務!""") 62 | ) -------------------------------------------------------------------------------- /services/video_service.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | 用戶上傳照片時,將照片從Line取回,放入CloudStorage 4 | 5 | 瀏覽用戶目前擁有多少張照片(未) 6 | 7 | ''' 8 | 9 | from models.user import User 10 | from flask import Request 11 | from linebot import ( 12 | LineBotApi 13 | ) 14 | 15 | import os 16 | from daos.user_dao import UserDAO 17 | from linebot.models import ( 18 | TextSendMessage 19 | ) 20 | 21 | 22 | # 圖片下載與上傳專用 23 | import urllib.request 24 | from google.cloud import storage 25 | 26 | 27 | class VideoService: 28 | line_bot_api = LineBotApi(channel_access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"]) 29 | 30 | ''' 31 | 用戶上傳照片 32 | 將照片取回 33 | 將照片存入CloudStorage內 34 | ''' 35 | @classmethod 36 | def line_user_upload_video(cls,event): 37 | 38 | # 取出照片 39 | image_blob = cls.line_bot_api.get_message_content(event.message.id) 40 | temp_file_path=f"""{event.message.id}.mp4""" 41 | 42 | # 43 | with open(temp_file_path, 'wb') as fd: 44 | for chunk in image_blob.iter_content(): 45 | fd.write(chunk) 46 | 47 | # 上傳至bucket 48 | storage_client = storage.Client() 49 | bucket_name = os.environ['USER_INFO_GS_BUCKET_NAME'] 50 | destination_blob_name = f'{event.source.user_id}/video/{event.message.id}.mp4' 51 | bucket = storage_client.bucket(bucket_name) 52 | blob = bucket.blob(destination_blob_name) 53 | blob.upload_from_filename(temp_file_path) 54 | 55 | # 移除本地檔案 56 | os.remove(temp_file_path) 57 | 58 | # 回覆消息 59 | cls.line_bot_api.reply_message( 60 | event.reply_token, 61 | TextSendMessage(f"""影片已上傳,請期待未來的AI服務!""") 62 | ) -------------------------------------------------------------------------------- /controllers/line_bot_controller.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 當用戶關注時,必須取用照片,並存放至指定bucket位置,而後生成User物件,存回db 3 | 當用戶取消關注時, 4 | 從資料庫提取用戶數據,修改用戶的封鎖狀態後,存回資料庫 5 | ''' 6 | 7 | from linebot import ( 8 | LineBotApi, WebhookHandler 9 | ) 10 | import os 11 | 12 | # 載入Follow事件 13 | from linebot.models.events import ( 14 | FollowEvent, UnfollowEvent 15 | ) 16 | 17 | from services.image_service import ImageService 18 | from services.user_service import UserService 19 | from services.video_service import VideoService 20 | from services.audio_service import AudioService 21 | 22 | from urllib.parse import parse_qs 23 | 24 | 25 | class LineBotController: 26 | 27 | # 將消息交給用戶服務處理 28 | @classmethod 29 | def follow_event(cls, event): 30 | # print(event) 31 | UserService.line_user_follow(event) 32 | 33 | @classmethod 34 | def unfollow_event(cls, event): 35 | UserService.line_user_unfollow(event) 36 | 37 | # 未來可能會判斷用戶快取狀態 38 | # 現在暫時無 39 | @classmethod 40 | def handle_text_message(cls, event): 41 | 42 | return None 43 | 44 | # 用戶收到照片時的處理辦法 45 | @classmethod 46 | def handle_image_message(cls, event): 47 | ImageService.line_user_upload_image(event) 48 | return "OK" 49 | 50 | # 用戶收到照片時的處理辦法 51 | @classmethod 52 | def handle_video_message(cls, event): 53 | VideoService.line_user_upload_video(event) 54 | return "OK" 55 | 56 | @classmethod 57 | def handle_audio_message(cls, event): 58 | AudioService.line_user_upload_video(event) 59 | return "OK" 60 | 61 | # 擷取event的data欄位,並依照function_name,丟入不同的方法 62 | @classmethod 63 | def handle_postback_event(cls, event): 64 | 65 | # query string 拆解 event.postback.data 66 | query_string_dict = parse_qs(event.postback.data) 67 | 68 | # 擷取功能 69 | detect_function_name = query_string_dict.get('function_name')[0] 70 | 71 | # Postbakc function 功能對應轉發 72 | 73 | 74 | return 'no' -------------------------------------------------------------------------------- /utils/reply_send_message.py: -------------------------------------------------------------------------------- 1 | # 引用會用到的套件 2 | from linebot.models import ( 3 | ImagemapSendMessage,TextSendMessage,ImageSendMessage,LocationSendMessage,FlexSendMessage,VideoSendMessage,StickerSendMessage,AudioSendMessage 4 | ) 5 | 6 | from linebot.models.template import ( 7 | ButtonsTemplate,CarouselTemplate,ConfirmTemplate,ImageCarouselTemplate 8 | 9 | ) 10 | 11 | from linebot.models.template import * 12 | 13 | import json 14 | 15 | def detect_json_array_to_new_message_array(fileName): 16 | 17 | #開啟檔案,轉成json 18 | with open(fileName,encoding='utf8') as f: 19 | jsonArray = json.load(f) 20 | 21 | # 解析json 22 | returnArray = [] 23 | for jsonObject in jsonArray: 24 | 25 | # 讀取其用來判斷的元件 26 | message_type = jsonObject.get('type') 27 | 28 | # 轉換 29 | if message_type == 'text': 30 | returnArray.append(TextSendMessage.new_from_json_dict(jsonObject)) 31 | elif message_type == 'imagemap': 32 | returnArray.append(ImagemapSendMessage.new_from_json_dict(jsonObject)) 33 | elif message_type == 'template': 34 | returnArray.append(TemplateSendMessage.new_from_json_dict(jsonObject)) 35 | elif message_type == 'image': 36 | returnArray.append(ImageSendMessage.new_from_json_dict(jsonObject)) 37 | elif message_type == 'sticker': 38 | returnArray.append(StickerSendMessage.new_from_json_dict(jsonObject)) 39 | elif message_type == 'audio': 40 | returnArray.append(AudioSendMessage.new_from_json_dict(jsonObject)) 41 | elif message_type == 'location': 42 | returnArray.append(LocationSendMessage.new_from_json_dict(jsonObject)) 43 | elif message_type == 'flex': 44 | returnArray.append(FlexSendMessage.new_from_json_dict(jsonObject)) 45 | elif message_type == 'video': 46 | returnArray.append(VideoSendMessage.new_from_json_dict(jsonObject)) 47 | 48 | 49 | # 回傳 50 | return returnArray 51 | -------------------------------------------------------------------------------- /models/user.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 基於Line給的用戶屬性,定義用戶類別 3 | 並提供 to_dict, from_dict方法,使能在object與dict間快速轉換 4 | 提供 __repr__ 快速打印參數 5 | ''' 6 | from __future__ import annotations 7 | 8 | 9 | class User(object): 10 | 11 | # 物件基礎建構式 12 | def __init__(self, line_user_id, line_user_pic_url, line_user_nickname, line_user_status, line_user_system_language, 13 | blocked=False): 14 | self.line_user_id = line_user_id 15 | self.line_user_pic_url = line_user_pic_url 16 | self.line_user_nickname = line_user_nickname 17 | self.line_user_status = line_user_status 18 | self.line_user_system_language = line_user_system_language 19 | self.blocked = blocked 20 | 21 | # source的欄位係以 資料庫欄位做為預設命名 22 | @staticmethod 23 | def from_dict(source: dict) -> User: 24 | user = User( 25 | line_user_id=source.get(u'line_user_id'), 26 | line_user_pic_url=source.get(u'line_user_pic_url'), 27 | line_user_nickname=source.get(u'line_user_nickname'), 28 | line_user_status=source.get(u'line_user_status'), 29 | line_user_system_language=source.get(u'line_user_system_language'), 30 | blocked=source.get(u'blocked') 31 | ) 32 | 33 | return user 34 | 35 | def to_dict(self): 36 | user_dict = { 37 | "line_user_id": self.line_user_id, 38 | "line_user_pic_url": self.line_user_pic_url, 39 | "line_user_nickname": self.line_user_nickname, 40 | "line_user_status": self.line_user_status, 41 | "line_user_system_language": self.line_user_system_language, 42 | "blocked": self.blocked 43 | } 44 | return user_dict 45 | 46 | def __repr__(self): 47 | return (f'''User( 48 | line_user_id={self.line_user_id}, 49 | line_user_pic_url={self.line_user_pic_url}, 50 | line_user_nickname={self.line_user_nickname}, 51 | line_user_status={self.line_user_status}, 52 | line_user_system_language={self.line_user_system_language}, 53 | blocked={self.blocked} 54 | )''' 55 | ) -------------------------------------------------------------------------------- /services/user_service.py: -------------------------------------------------------------------------------- 1 | ''' 2 | line用戶關注時,儲存line用戶個資時,取照片,更換連結,放回原object, 3 | line用戶封鎖時,將資料庫內的用戶資料更改為已封鎖。 4 | 取出用戶個資時,傳入用戶id,作為檢索條件 5 | ''' 6 | 7 | from models.user import User 8 | from flask import Request 9 | from linebot import ( 10 | LineBotApi 11 | ) 12 | 13 | import os 14 | from daos.user_dao import UserDAO 15 | 16 | # 圖片下載與上傳專用 17 | import urllib.request 18 | from google.cloud import storage 19 | 20 | 21 | class UserService: 22 | line_bot_api = LineBotApi(channel_access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"]) 23 | 24 | ''' 25 | 取得line event,將line event拿去取個資,轉換成User,並將其照片取出,存回cloud storage, 26 | 並用cloudstorage的連結取代user的line圖片連結 27 | ''' 28 | 29 | @classmethod 30 | def line_user_follow(cls, event): 31 | # 取個資 32 | line_user_profile = cls.line_bot_api.get_profile(event.source.user_id) 33 | # print(line_user_profile) 34 | 35 | # 將個資轉換成user 36 | user = User( 37 | line_user_id=line_user_profile.user_id, 38 | line_user_pic_url=line_user_profile.picture_url, 39 | line_user_nickname=line_user_profile.display_name, 40 | line_user_status=line_user_profile.status_message, 41 | line_user_system_language=line_user_profile.language, 42 | blocked=False 43 | ) 44 | 45 | ''' 46 | # , 47 | 先確認用戶的照片連結是否正常, 48 | 若存在,取得用戶照片,存放回cloud storage 49 | 並將連結存回user的連結 50 | ''' 51 | if user.line_user_pic_url is not None: 52 | # 跟line 取回照片,並放置在本地端 53 | file_name = user.line_user_id + '.jpg' 54 | urllib.request.urlretrieve(user.line_user_pic_url, file_name) 55 | 56 | # 上傳至bucket 57 | storage_client = storage.Client() 58 | bucket_name = os.environ['USER_INFO_GS_BUCKET_NAME'] 59 | destination_blob_name = f'{user.line_user_id}/user_pic.png' 60 | bucket = storage_client.bucket(bucket_name) 61 | blob = bucket.blob(destination_blob_name) 62 | blob.upload_from_filename(file_name) 63 | 64 | # 更新回user的圖片連結 65 | destination_url = f'https://storage.googleapis.com/{bucket_name}/{user.line_user_id}/user_pic.png' 66 | user.line_user_pic_url = destination_url 67 | 68 | # 存入資料庫 69 | UserDAO.save_user(user) 70 | 71 | # 打印結果 72 | # print(user) 73 | 74 | # 回傳結果給handler 75 | # 關注的部分,不回傳,交由控制台回傳 76 | pass 77 | 78 | # 從資料庫內取出用戶資料,並將其blocked狀態,更改為True 79 | @classmethod 80 | def line_user_unfollow(cls, event): 81 | user = UserDAO.get_user(event.source.user_id) 82 | user.blocked = True 83 | UserDAO.save_user(user) 84 | # print(user) 85 | # print('用戶已封鎖') 86 | 87 | pass 88 | 89 | # 依照用戶id,取回用戶資料 90 | @classmethod 91 | def get_user(cls, user_id: str): 92 | user = UserDAO.get_user(user_id) 93 | # print(user) 94 | return user -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | curPath = os.path.abspath(os.path.dirname(__file__)) 4 | rootPath = os.path.split(curPath)[0] 5 | sys.path.append(rootPath) 6 | 7 | from flask import Flask, request, abort 8 | 9 | from flask_cors import CORS 10 | 11 | 12 | from linebot.exceptions import ( 13 | InvalidSignatureError 14 | ) 15 | 16 | 17 | from controllers.line_bot_controller import LineBotController 18 | 19 | from controllers.user_controller import UserController 20 | 21 | app = Flask(__name__) 22 | CORS(app) 23 | 24 | from linebot import ( 25 | LineBotApi, WebhookHandler 26 | ) 27 | import os 28 | line_bot_api=LineBotApi(channel_access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"]) 29 | handler=WebhookHandler(channel_secret=os.environ["LINE_CHANNEL_SECRET"]) 30 | 31 | 32 | # 載入Follow事件 33 | from linebot.models.events import ( 34 | FollowEvent,UnfollowEvent,MessageEvent,TextMessage,PostbackEvent,ImageMessage,AudioMessage,VideoMessage 35 | ) 36 | 37 | 38 | # 建立日誌紀錄設定檔 39 | # https://googleapis.dev/python/logging/latest/stdlib-usage.html 40 | import logging 41 | import google.cloud.logging 42 | from google.cloud.logging.handlers import CloudLoggingHandler 43 | 44 | 45 | client = google.cloud.logging.Client() 46 | 47 | # 建立line event log,用來記錄line event 48 | bot_event_handler = CloudLoggingHandler(client,name="ncu_bot_event") 49 | bot_event_logger=logging.getLogger('ncu_bot_event') 50 | bot_event_logger.setLevel(logging.INFO) 51 | bot_event_logger.addHandler(bot_event_handler) 52 | 53 | app = Flask(__name__) 54 | 55 | @app.route('/test') 56 | def hello_world(): 57 | bot_event_logger.info("test") 58 | return 'Hello, World!' 59 | 60 | ''' 61 | 轉發功能列表 62 | ''' 63 | @app.route("/callback", methods=['POST']) 64 | def callback(): 65 | # get X-Line-Signature header value 66 | signature = request.headers['X-Line-Signature'] 67 | # get request body as text 68 | body = request.get_data(as_text=True) 69 | bot_event_logger.info(body) 70 | # print(body) 71 | # handle webhook body 72 | try: 73 | handler.handle(body, signature) 74 | except InvalidSignatureError: 75 | print("Invalid signature. Please check your channel access token/channel secret.") 76 | abort(400) 77 | 78 | return 'OK' 79 | 80 | @handler.add(FollowEvent) 81 | def handle_line_follow(event): 82 | return LineBotController.follow_event(event) 83 | 84 | @handler.add(UnfollowEvent) 85 | def handle_line_unfollow(event): 86 | return LineBotController.unfollow_event(event) 87 | 88 | @handler.add(MessageEvent,TextMessage) 89 | def handle_line_text(event): 90 | return LineBotController.handle_text_message(event) 91 | 92 | @handler.add(MessageEvent,ImageMessage) 93 | def handle_line_image(event): 94 | return LineBotController.handle_image_message(event) 95 | 96 | @handler.add(MessageEvent,VideoMessage) 97 | def handle_line_video(event): 98 | return LineBotController.handle_video_message(event) 99 | 100 | @handler.add(MessageEvent,AudioMessage) 101 | def handle_line_audio(event): 102 | return LineBotController.handle_audio_message(event) 103 | 104 | @handler.add(PostbackEvent) 105 | def handle_postback_event(event): 106 | return LineBotController.handle_postback_event(event) 107 | 108 | @app.route("/user",methods=['GET']) 109 | def get_user(): 110 | result = UserController.get_user(request) 111 | return result 112 | 113 | if __name__ == "__main__": 114 | app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) -------------------------------------------------------------------------------- /services/image_service.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | 用戶上傳照片時,將照片從Line取回,放入CloudStorage 4 | 5 | 瀏覽用戶目前擁有多少張照片(未) 6 | 7 | ''' 8 | 9 | from models.user import User 10 | from flask import Request 11 | from linebot import ( 12 | LineBotApi 13 | ) 14 | 15 | import os 16 | from daos.user_dao import UserDAO 17 | from linebot.models import ( 18 | TextSendMessage 19 | ) 20 | 21 | 22 | # 圖片下載與上傳專用 23 | import urllib.request 24 | from google.cloud import storage 25 | 26 | # 圖像辨識 27 | import tensorflow.keras 28 | from PIL import Image, ImageOps 29 | import numpy as np 30 | import time 31 | 32 | import os 33 | 34 | from utils.reply_send_message import detect_json_array_to_new_message_array 35 | 36 | model = tensorflow.keras.models.load_model('converted_savedmodel/model.savedmodel') 37 | 38 | class ImageService: 39 | line_bot_api = LineBotApi(channel_access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"]) 40 | 41 | ''' 42 | 用戶上傳照片 43 | 將照片取回 44 | 將照片存入CloudStorage內 45 | ''' 46 | @classmethod 47 | def line_user_upload_image(cls,event): 48 | 49 | # 取出照片 50 | image_blob = cls.line_bot_api.get_message_content(event.message.id) 51 | temp_file_path=f"""{event.message.id}.png""" 52 | 53 | # 54 | with open(temp_file_path, 'wb') as fd: 55 | for chunk in image_blob.iter_content(): 56 | fd.write(chunk) 57 | 58 | # 上傳至bucket 59 | storage_client = storage.Client() 60 | bucket_name = os.environ['USER_INFO_GS_BUCKET_NAME'] 61 | destination_blob_name = f'{event.source.user_id}/image/{event.message.id}.png' 62 | bucket = storage_client.bucket(bucket_name) 63 | blob = bucket.blob(destination_blob_name) 64 | blob.upload_from_filename(temp_file_path) 65 | 66 | 67 | 68 | # 載入模型Label 69 | ''' 70 | 載入類別列表 71 | ''' 72 | class_dict = {} 73 | with open('converted_savedmodel/labels.txt') as f: 74 | for line in f: 75 | (key, val) = line.split() 76 | class_dict[int(key)] = val 77 | 78 | # 載入模型 79 | # Disable scientific notation for clarity 80 | np.set_printoptions(suppress=True) 81 | 82 | # Load the model 83 | # model = tensorflow.keras.models.load_model('converted_savedmodel/model.savedmodel') 84 | 85 | # 圖片預測 86 | data = np.ndarray(shape=(1, 224, 224, 3), dtype=np.float32) 87 | image = Image.open(temp_file_path) 88 | size = (224, 224) 89 | image = ImageOps.fit(image, size, Image.ANTIALIAS) 90 | image_array = np.asarray(image) 91 | # Normalize the image 92 | normalized_image_array = (image_array.astype(np.float32) / 127.0 - 1 ) 93 | 94 | # Load the image into the array 95 | data = np.ndarray(shape=(1, 224, 224, 3), dtype=np.float32) 96 | data[0]= normalized_image_array[0:224,0:224,0:3] 97 | 98 | # run the inference 99 | prediction = model.predict(data) 100 | 101 | # 取得預測值 102 | max_probability_item_index = np.argmax(prediction[0]) 103 | 104 | 105 | # 將預測值拿去尋找line_message 106 | # 並依照該line_message,進行消息回覆 107 | if prediction.max() > 0.6: 108 | result_message_array = detect_json_array_to_new_message_array("line_message_json/"+class_dict.get(max_probability_item_index)+".json") 109 | cls.line_bot_api.reply_message( 110 | event.reply_token, 111 | result_message_array 112 | ) 113 | else: 114 | cls.line_bot_api.reply_message( 115 | event.reply_token, 116 | TextSendMessage(f"""圖片無法辨認,圖片已上傳,請期待未來的AI服務!""") 117 | ) 118 | 119 | # 移除本地檔案 120 | os.remove(temp_file_path) 121 | 122 | # 回覆消息 123 | # cls.line_bot_api.reply_message( 124 | # event.reply_token, 125 | # TextSendMessage(f"""圖片已上傳,請期待未來的AI服務!""") 126 | # ) --------------------------------------------------------------------------------