Pre-trained AI를 활용해서 Serverless로 Image에서 Text를 추출해 번역하는 앱을 Google Cloud에서 구축하려고 해봤다.
Cloud Run으로 클라이언트 앱을 호스팅하고 Cloud Functions로 백엔드 로직을 실행시키며, Google의 pre-trained된 AI를 사용할 수 있는 API를 활용합니다.
1. 사용자 입력을 수집하고 이미치를 캡처해 백엔드(Cloud function)으로 전달하는 Flask 웹 앱을 생성합니다.
2. Flask 웹 앱을 Dockerize 하여 Container Registry 에 push 합니다.
1. Google Cloud API를 호출해 이미지에서 텍스트를 추출합니다.(Cloud Vision)
2. 추출한 텍스트를 활용해 번역 API를 호출합니다.(Cloud Translation)
3. 번역된 내용을 Flask 웹 앱에 전달합니다.(Cloud Run)
Gemini Pro 와 같은 멀티모달 Gen AI를 활용해도 비슷한 결과가 나오지만, 이것은 자연어 처리와 복잡한 상호 작용이 필요하지 않고 In / Out으로 필요한 것이 아주 명확하기 때문에 Vision API와 Translate API를 활용했습니다.
(Vision API는 한 달에 1000개의 무료 호출을 허용하고, Translate API는 한 달에 처음 50만 개의 문자를 무료로 번역 가능합니다.)
코드의 구조는 아래과 같습니다.
└── image-text-translator
├── scripts/ - 환경 설정 및 도우미 스크립트
|└── setup.sh - 설정 도우미 스크립트
├── app/ - 애플리케이션
│ ├── ui_cr/ - 브라우저 UI(Cloud Run)
│ │ ├── static/ - 프런트엔드용 정적 콘텐츠
| | ├── templates/ - 프런트엔드용 HTML 템플릿
| | ├── app.py - Flask 애플리케이션
| | ├── requirements.txt - UI Python requirements
| | ├── Dockerfile - Flask 컨테이너를 빌드하는 Dockerfile
| | └── .dockerignore - Dockerfile에서 무시할 파일
| |
│ └── backend_gcf/ - 백엔드(클라우드 함수)
│ ├── main.py - 백엔드 CF 애플리케이션
│ └── requirements.txt - 백엔드 CF Python 요구 사항
├── requirements.txt - 프로젝트 로컬 개발에 대한 Python 요구 사항
└── README.md
1. Google Cloud에서 프로젝트를 만들고 API를 활성화 합니다.
# Google Cloud에 인증
gcloud auth list
# 올바른 프로젝트를 선택했는지 확인
export PROJECT_ID=<프로젝트 ID 입력>
gcloud config set project $PROJECT_ID
# Cloud Build API 사용
gcloud services enable cloudbuild.googleapis.com
# Cloud Storage API 사용
gcloud services enable storage-api.googleapis.com
# Artifact Registry API 사용
gcloud services enable artifactregistry.googleapis.com
# Eventarc API 사용
gcloud services enable eventarc.googleapis.com
# Cloud Run Admin API 사용
gcloud services enable run.googleapis.com
# Cloud Logging API 사용
gcloud services enable logging.googleapis.com
# Cloud Pub/Sub API 사용
gcloud services enable pubsub.googleapis.com #
Cloud Functions API
사용 gcloud services enable cloudfunctions.googleapis.com
# Cloud Translation API 사용
gcloud services enable translate.googleapis.com
# Cloud Vision API 사용
gcloud services enable vision.googleapis.com
# 서비스 계정 자격 증명 API 사용
gcloud services enable iamcredentials.googleapis.com
2. 인증 및 권한 부여를 해야 함.(Cloud Run은 Cloud Function에 인증해야 하고 Cloud Function은 Cloud Vision, Translation API에 인증해야 함)
# 이 작업을 하기 전에 PROJECT_ID 변수가 설정되어 있는지 확인하세요!
export SVC_ACCOUNT=image-text-translator-sa
export SVC_ACCOUNT_EMAIL= $SVC_ACCOUNT @ $PROJECT_ID .iam.gserviceaccount.com
# 사용자 관리 서비스 계정을 연결하는 것은 Google Cloud에서 실행되는 프로덕션 코드에 대한 ADC에 자격 증명을 제공하는 가장 좋은 방법입니다.
gcloud iam service-accounts create $SVC_ACCOUNT
3. 이제 서비스 계정의 여러 역할들을 묶어주겠습니다.
# 서비스 계정에 역할 부여
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SVC_ACCOUNT_EMAIL" \
--role=roles/run.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SVC_ACCOUNT_EMAIL" \
--role=roles/run.invoker
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SVC_ACCOUNT_EMAIL" \
--role=roles/cloudfunctions.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SVC_ACCOUNT_EMAIL" \
--role=roles/cloudfunctions.invoker
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SVC_ACCOUNT_EMAIL" \
--role="roles/cloudtranslate.user"
# 서비스 계정을 다른 리소스에 연결할 주체에게 필요한 역할을 부여합니다.
gcloud iam service-accounts add-iam-policy-binding $SVC_ACCOUNT_EMAIL \
--member="group:gcp-devops@my-org.com" \
--role=roles/iam.serviceAccountUser
# 서비스 계정 허용
gcloud iam service-accounts add-iam-policy-binding $SVC_ACCOUNT_EMAIL \
--member="group:gcp-devops@my-org.com" \
--role=roles/iam.serviceAccountTokenCreator
# 계정에 Cloud Functions 및 Cloud Run에 배포할 수 있는 액세스 권한이 있는지 확인합니다.
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="group:gcp-devops@my-org.com" \
--role roles/run.admin
4. 로컬 개발 환경을 열고 GCP와 연결합니다.
# 로컬 Linux 환경에 Google Cloud CLI를 설치합니다.
# https://cloud.google.com/sdk/docs/install을 참조하세요. #
Gcloud CLI에서 Python 및 pip를 설정합니다.
# https://cloud.google.com/python/docs/setup을 참조하세요.
# 로컬 개발자를 위한 추가 Google Cloud CLI 패키지를 설치합니다.
sudo apt install google-cloud-cli-gke-gcloud-auth-plugin kubectl google-cloud-cli-skaffold google-cloud-cli-minikube
# Google Cloud에 인증합니다.
gcloud auth login
5. git 을 복사하고 가상환경을 생성해 필요한 라이브러리를 설치합니다.
git clone https://github.com/hanhyeonkyu/image-text-translator.git
cd image-text-translator
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
6. 앱 기본 자격 증명(ADC) 설정
ADC는 인증 라이브러리가 환경에 따라 자동으로 자격 증명을 찾을 수 있도록 하는 설정입니다. 로컬 환경과 Google Cloud의 대상 환경에서 모두 ADC를 활용할 수 있습니다.
ADC는 서비스 계정 자격 증명을 사용하도록 구성할 수 있습니다.
Cloud Run에서 Cloud Function 호출을 인증할 때, ADC를 가리킬 수 있는 서비스 계정 키를 만들었습니다.
gcloud auth application-default login
# 아직 설정하지 않은 경우...
export SVC_ACCOUNT=image-text-translator-sa
export SVC_ACCOUNT_EMAIL=$SVC_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
# 로컬 개발자를 위한 서비스 계정 키 생성
gcloud iam service-accounts keys create ~/.config/gcloud/$SVC_ACCOUNT.json \
--iam-account=$SVC_ACCOUNT_EMAIL
# 클라이언트 라이브러리에서 자동으로 감지되는 ADC 환경변수
export GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/$SVC_ACCOUNT.json
7. 모든 터미널에서 설정해야 하는 명령들
export PROJECT_ID=$(gcloud config list --format='value(core.project)')
export REGION=europe-west4
export SVC_ACCOUNT=image-text-translator-sa
export SVC_ACCOUNT_EMAIL=$SVC_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
export GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/$SVC_ACCOUNT.json
# Functions
export FUNCTIONS_PORT=8081
export BACKEND_GCF=https://$REGION-$PROJECT_ID.cloudfunctions.net/extract-and-translate
# Flask
export FLASK_SECRET_KEY=some-secret-1234
export FLASK_RUN_PORT=8080
echo "Environment variables configured:"
echo PROJECT_ID="$PROJECT_ID"
echo REGION="$REGION"
echo SVC_ACCOUNT_EMAIL="$SVC_ACCOUNT_EMAIL"
echo BACKEND_GCF="$BACKEND_GCF"
echo FUNCTIONS_PORT="$FUNCTIONS_PORT"
echo FLASK_RUN_PORT="$FLASK_RUN_PORT"
main.py에 아래 함수를 만듭니다. extract_and_translate가 Cloud Functions에 진입점 역할을 합니다.
@functions_framework.http
def extract_and_translate(request):
"""Extract and translate the text from an image.
The image can be POSTed in the request, or it can be a GCS object reference.
If a POSTed image, enctype should be multipart/form-data and the file named 'uploaded'.
If we're passing a GCS object reference, content-type should be 'application/json',
with two attributes:
- bucket: name of GCS bucket in which the file is stored.
- filename: name of the file to be read.
"""
# Check if the request method is POST
if request.method == 'POST':
# Get the uploaded file from the request
uploaded = request.files.get('uploaded') # Assuming the input filename is 'uploaded'
to_lang = request.form.get('to_lang', "en")
print(f"{uploaded=}, {to_lang=}")
if not uploaded:
return flask.jsonify({"error": "No file uploaded."}), 400
if uploaded: # Process the uploaded file
file_contents = uploaded.read() # Read the file contents
image = vision.Image(content=file_contents)
else:
return flask.jsonify({"error": "Unable to read uploaded file."}), 400
else:
# If we haven't created this, then get it from the bucket instead
content_type = request.headers.get('content-type', 'null')
if content_type == 'application/json':
bucket = request.json.get('bucket', None)
filename = request.json.get('filename', None)
to_lang = request.json.get('to_lang', "en")
print(f"Received {bucket=}, {filename=}, {to_lang=}")
else:
return flask.jsonify({"error": "Unknown content type."}), 400
if bucket:
image = vision.Image(source=vision.ImageSource(gcs_image_uri=f"gs://{bucket}/{filename}"))
# Use the Vision API to extract text from the image
detected = detect_text(image)
if detected:
translated = translate_text(detected, to_lang)
if translated["text"] != "":
# print(translated)
return translated["text"]
return "No text found in the image."
- POST 를 받았는지 체크하고, uploaded 객체를 찾은 뒤 이를 이진수로 읽어 vision.Image 객체를 생성합니다.
- 다음으로 이미지를 전달할 함수를 호출합니다.(detect_text)
def detect_text(image: vision.Image) -> dict | None:
"""Extract the text from the Image object """
text_detection_response = vision_client.text_detection(image=image)
annotations = text_detection_response.text_annotations
if annotations:
text = annotations[0].description
else:
text = ""
# print(f"Extracted text from image:\n{text}")
# Returns language identifer in ISO 639-1 format. E.g. en.
# See https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
detect_language_response = translate_client.detect_language(text)
src_lang = detect_language_response["language"]
print(f"Detected language: {src_lang}.")
message = {
"text": text,
"src_lang": src_lang,
}
return message
이미지 속의 텍스트를 감지하고 Google 언어 API를 사용해 텍스트의 언어가 어느 나라 언어인지도 판별합니다.
다음으로 translate_text 기능입니다.
def translate_text(message: dict, to_lang: str) -> dict:
"""
Translates the text in the message from the specified source language
to the requested target language, then sends a message requesting another
service save the result.
"""
text = message["text"]
src_lang = message["src_lang"]
translated = { # before translating
"text": text,
"src_lang": src_lang,
"to_lang": to_lang,
}
if src_lang != to_lang and src_lang != "und":
print(f"Translating text into {to_lang}.")
translated_text = translate_client.translate(
text, target_language=to_lang, source_language=src_lang)
translated = {
"text": unescape(translated_text["translatedText"]),
"src_lang": src_lang,
"to_lang": to_lang,
}
else:
print("No translation required.")
return translated
언어 감지를 통해 src_lang을 파악했고 to_lang을 통해 어느 나라 언어로 번역할지를 선택한 뒤 Google Translate API를 사용해 번역합니다.
로컬에서 테스트하려면 backend-gcf 폴더에서 명령을 실행하세요.
# Run the function
functions-framework --target extract_and_translate \
--debug --port $FUNCTIONS_PORT
이미지로 테스트 해보려면 아래와 같이 실행하세요.(원하는 이미지를 이용하세요.)
# You will first need to authenticate and set the environment vars in this terminal
source ./scripts/setup.sh
# now invoke
curl -X POST localhost:$FUNCTIONS_PORT \
-H "Content-Type: multipart/form-data" \
-F "uploaded=@./testing/images/kr_meme.jpg" \
-F "to_lang=en"
실행된 것을 확인할 수 있습니다.
backend-gcf 폴더에서 아래 명령을 실행하세요.
# From the backend-gcf folder
gcloud functions deploy extract-and-translate \
--gen2 --max-instances 1 \
--region $REGION \
--runtime=python312 --source=. \
--trigger-http --entry-point=extract_and_translate \
--no-allow-unauthenticated
# Allow this function to be called by the service account
gcloud functions add-invoker-policy-binding extract-and-translate \
--region=$REGION \
--member="serviceAccount:$SVC_ACCOUNT_EMAIL"
VS Code의 Cloud Code 확장 기능을 사용하면 배포된 Cloud Function 을 직관적으로 확인할 수 있습니다.
이제 Cloud Function에서 확인해 봅니다.
curl -X POST https://$REGION-$PROJECT_ID.cloudfunctions.net/extract-and-translate \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: multipart/form-data" \
-F "uploaded=@./testing/images/ua_meme.jpg" \
-F "to_lang=en"
잘 작동되는 것을 확인할 수 있습니다.
Cloud Function에 버전을 업데이트 하고 싶다면 deploy 명령을 다시 실행하면 됩니다.
또한 Cloud Function을 삭제하려면 아래와 같이 실행하세요.
gcloud functions delete extract-and-translate --region=$REGION
이제 Cloud Run 에 Flask Web APP을 배포해야 하는데요.
Flask Web App은 단순히 이미지를 업로드하고 Cloud Function에 요청하는 것이니 Github 코드를 참고하세요.
이제 Cloud Run에 배포합니다.
Flask Web App을 Google Artifact Registry(GAR)에 저장하기 위해 저장소를 먼저 생성합니다.
Cloud Build를 사용해 소스에서 컨테이너 이미지를 빌드하고 GAR에 저장합니다.
GAR의 이미지를 참조해 Cloud Run에서 배포합니다.
그럼 먼저 이미지를 빌드해 줍니다.
export IMAGE_NAME=$REGION-docker.pkg.dev/$PROJECT_ID/image-text-translator-artifacts/image-text-translator-ui
# configure Docker to use the Google Cloud CLI to authenticate requests to Artifact Registry.
gcloud auth configure-docker $REGION-docker.pkg.dev
# Build the image and push it to Artifact Registry
# Run from the ui_cr folder
gcloud builds submit --tag $IMAGE_NAME:v0.1 .
이제 이미지를 Cloud Run으로 배포합니다.
# create a random secret key for our Flask application
export RANDOM_SECRET_KEY=$(openssl rand -base64 32)
gcloud run deploy image-text-translator-ui \
--image=$IMAGE_NAME:v0.1 \
--region=$REGION \
--platform=managed \
--allow-unauthenticated \
--max-instances=1 \
--service-account=$SVC_ACCOUNT \
--set-env-vars BACKEND_GCF=$BACKEND_GCF,FLASK_SECRET_KEY=$RANDOM_SECRET_KEY
새 버전의 배포를 원한다면 아래와 같이 하면 됩니다.
# Check our IMAGE_NAME is set
export IMAGE_NAME=$REGION-docker.pkg.dev/$PROJECT_ID/image-text-translator-artifacts/image-text-translator-ui
# Set our new version number
export VERSION=v0.2
# Rebuild the container image and push to the GAR
gcloud builds submit --tag $IMAGE_NAME:$VERSION .
# create a random secret key for our Flask application
export RANDOM_SECRET_KEY=$(openssl rand -base64 32)
# Redeploy
gcloud run deploy image-text-translator-ui \
--image=$IMAGE_NAME:$VERSION \
--region=$REGION \
--platform=managed \
--allow-unauthenticated \
--max-instances=1 \
--service-account=$SVC_ACCOUNT \
--set-env-vars BACKEND_GCF=$BACKEND_GCF,FLASK_SECRET_KEY=$RANDOM_SECRET_KEY
자동으로 생성된 DNS가 있지만 미리 준비된 다른 도메인과 맵핑하려면 아래와 같습니다.
# Verify your domain ownership with Google
gcloud domains verify mydomain.com
# Check it
gcloud domains list-user-verified
# Create a mapping to your domain
gcloud beta run domain-mappings create \
--region $REGION \
--service image-text-translator-ui \
--domain image-text-translator.mydomain.com
# Obtain the DNS records. We want everything under `resourceRecords`.
gcloud beta run domain-mappings describe \
--region $REGION \
--domain image-text-translator.mydomain.com
이제 맵핑한 도메인에서 서비스가 운영되고 있는 것을 확인할 수 있습니다.
Golang에는 Set이 없어요... (0) | 2024.08.29 |
---|---|
CrowdStrike BSOD 에러 수정(cloud) (0) | 2024.07.25 |
paramiko ssh stdin.write 명령어 사용 시 OSError: socket is closed 발생 (0) | 2024.07.02 |
🏆 2024년 4월 가장 인기 있는 필수 솔루션 10개 (0) | 2024.05.24 |
NestJS, Fastify, Apollo GraphQL & NextJS를 사용해 채팅 앱 구축(클라이언트편) (0) | 2024.05.21 |
댓글 영역