api 구축

This commit is contained in:
2025-06-04 15:25:36 +09:00
parent 04536eabd6
commit 5510529a36
7 changed files with 698 additions and 0 deletions

139
workspace/api.py Normal file
View File

@@ -0,0 +1,139 @@
# api.py
import asyncio
import json # JSON 파싱을 위해 추가
from fastapi import APIRouter, FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from routers import google_docai
from utils.config import (
CORS_ALLOW_CREDENTIALS,
CORS_ALLOW_HEADERS,
CORS_ALLOW_METHODS,
CORS_ALLOW_ORIGINS,
UPLOAD_DOCS_DIR,
)
# 유틸리티 함수 임포트 (기존 코드 유지)
from utils.file_utils import (
create_essential_directories,
create_key,
save_uploaded_file,
)
app = FastAPI()
# CORS 설정 (기존 코드 유지)
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ALLOW_ORIGINS,
allow_credentials=CORS_ALLOW_CREDENTIALS,
allow_methods=CORS_ALLOW_METHODS,
allow_headers=CORS_ALLOW_HEADERS,
)
# 애플리케이션 시작 시 실행될 함수 (기존 코드 유지)
@app.on_event("startup")
async def startup_event():
print("Starting up...")
create_essential_directories()
print("Essential directories created.")
# --- Document AI 라우터 ---
doc_ai_router = APIRouter(
prefix="/docai",
tags=["DocumentAI"],
)
# Document AI 관련 설정값 (프로덕션에서는 환경 변수나 설정 파일에서 로드 권장)
DOCAI_PROJECT_ID = "drawingpdfocr-461103"
DOCAI_LOCATION = "us"
DOCAI_PROCESSOR_ID = "b838676d4e3b4758" # 실제 사용자의 프로세서 ID
async def run_sync_in_threadpool(func, *args, **kwargs):
"""동기 함수를 별도의 스레드에서 실행하고 await 가능하게 만듭니다."""
loop = asyncio.get_event_loop()
if hasattr(asyncio, "to_thread"): # Python 3.9+
return await asyncio.to_thread(func, *args, **kwargs)
else: # Python < 3.9
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
@doc_ai_router.post("/process-document/")
async def process_uploaded_document(file: UploadFile = File(...)):
"""
업로드된 파일을 Document AI로 처리하고, 추출된 엔티티 정보를 JSON으로 반환합니다.
"""
if not file.content_type:
raise HTTPException(status_code=400, detail="File content type is missing.")
# 지원되는 MIME 타입 (예시, 필요에 따라 확장)
allowed_mime_types = ["application/pdf", "image/jpeg", "image/png", "image/tiff"]
if file.content_type not in allowed_mime_types:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type: '{file.content_type}'. Supported: {', '.join(allowed_mime_types)}",
)
print(f"Received audio file for async processing: {file.filename}")
file_id = str(create_key())
# 파일 저장 (유틸리티 함수 사용)
try:
file_path, file_content = save_uploaded_file(file, UPLOAD_DOCS_DIR, file_id)
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(
status_code=500, detail=f"파일 저장 준비 중 오류 발생: {str(e)}"
)
try:
# Document AI 처리 (동기 함수를 비동기적으로 호출)
document_result = await run_sync_in_threadpool(
google_docai.process_document_from_content, # 수정된 함수 사용
project_id=DOCAI_PROJECT_ID,
location=DOCAI_LOCATION,
processor_id=DOCAI_PROCESSOR_ID,
file_content=file_content,
mime_type=file.content_type,
field_mask="text,entities", # 필요한 필드 마스크
)
print(document_result)
if not document_result:
# 이 경우는 process_document_from_content 함수 내부에서 예외가 발생하지 않고
# None이나 빈 Document 객체를 반환했을 때를 대비 (일반적으론 예외 발생)
raise HTTPException(
status_code=500,
detail="Failed to process document: No result from Document AI.",
)
json_output_string = google_docai.extract_and_convert_to_json(document_result)
return json.loads(json_output_string)
except HTTPException as http_exc:
# 이미 HTTPException으로 처리된 예외는 그대로 다시 발생시킴
raise http_exc
except Exception as e:
# 기타 예외 처리 (로깅 권장)
# import traceback
# print(f"Error processing file: {e}\n{traceback.format_exc()}")
raise HTTPException(
status_code=500,
detail=f"An error occurred during document processing: {str(e)}",
)
finally:
await file.close() # 업로드된 파일 객체를 닫아 리소스 정리
# app에 라우터 등록
app.include_router(doc_ai_router)
@app.get("/health/API")
async def health_check():
"""애플리케이션 상태 확인"""
return {"status": "API ok"}

View File

@@ -0,0 +1,70 @@
# google_docai.py
import json
import os
from typing import Optional
from google.api_core.client_options import ClientOptions
from google.cloud import documentai
if not os.getenv("GOOGLE_APPLICATION_CREDENTIALS"): # 이미 설정되어 있지 않다면
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = (
"/home/jackjack/test/doc_ai/workspace/drawingpdfocr-461103-2441e0b34216.json" # 이 경로가 API 서버 실행 시점에서 유효해야 함
)
def process_document_from_content( # 함수 이름 및 파라미터 변경
project_id: str,
location: str,
processor_id: str,
file_content: bytes, # file_path 대신 file_content (bytes)
mime_type: str,
field_mask: Optional[str] = None,
processor_version_id: Optional[str] = None,
) -> documentai.Document:
opts = ClientOptions(api_endpoint=f"{location}-documentai.googleapis.com")
client = documentai.DocumentProcessorServiceClient(client_options=opts)
if processor_version_id:
name = client.processor_version_path(
project_id, location, processor_id, processor_version_id
)
else:
name = client.processor_path(project_id, location, processor_id)
# 파일 읽기 부분이 사라지고, file_content를 직접 사용
raw_document = documentai.RawDocument(content=file_content, mime_type=mime_type)
# 예시: 첫 페이지만 처리 (필요에 따라 수정)
process_options = documentai.ProcessOptions(
individual_page_selector=documentai.ProcessOptions.IndividualPageSelector(
pages=[1]
)
)
request = documentai.ProcessRequest(
name=name,
raw_document=raw_document,
field_mask=field_mask,
process_options=process_options,
)
result = client.process_document(request=request)
document = result.document
return document
def extract_and_convert_to_json(
document: documentai.Document,
) -> str:
extracted_entities = []
if document and document.entities:
for entity in document.entities:
if (
hasattr(entity, "type_")
and hasattr(entity, "mention_text")
and entity.type_
and entity.mention_text
):
extracted_entities.append(
{"type": entity.type_, "mention_text": entity.mention_text}
)
return json.dumps(extracted_entities, ensure_ascii=False, indent=2)

104
workspace/tests/a.py Normal file
View File

@@ -0,0 +1,104 @@
import json # JSON 모듈 임포트
import os
from typing import Optional
from google.api_core.client_options import ClientOptions
from google.cloud import documentai # type: ignore
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = (
"../drawingpdfocr-461103-2441e0b34216.json" # 이 경로가 정확한지 다시 한번 확인해주세요!
)
def process_document_sample(
project_id: str,
location: str,
processor_id: str,
file_path: str,
mime_type: str,
field_mask: Optional[str] = None,
processor_version_id: Optional[str] = None,
) -> documentai.Document:
opts = ClientOptions(api_endpoint=f"{location}-documentai.googleapis.com")
client = documentai.DocumentProcessorServiceClient(client_options=opts)
if processor_version_id:
name = client.processor_version_path(
project_id, location, processor_id, processor_version_id
)
else:
name = client.processor_path(project_id, location, processor_id)
with open(file_path, "rb") as image:
image_content = image.read()
raw_document = documentai.RawDocument(content=image_content, mime_type=mime_type)
process_options = documentai.ProcessOptions(
individual_page_selector=documentai.ProcessOptions.IndividualPageSelector(
pages=[1]
)
)
request = documentai.ProcessRequest(
name=name,
raw_document=raw_document,
field_mask=field_mask,
process_options=process_options,
)
result = client.process_document(request=request)
document = result.document
return document
def extract_and_convert_to_json(
document: documentai.Document,
) -> str: # 반환 타입을 str (JSON 문자열)로 명시
"""
Document AI의 Document 객체에서 entities의 type과 mention_text를 추출하여
JSON 문자열로 반환합니다.
"""
extracted_entities = []
if document and document.entities:
for entity in document.entities:
if (
hasattr(entity, "type_")
and hasattr(
entity, "mention_text"
) # type_와 mention_text 속성이 있는지 확인
and entity.type_
and entity.mention_text
):
extracted_entities.append(
{"type": entity.type_, "mention_text": entity.mention_text}
)
return json.dumps(extracted_entities, ensure_ascii=False, indent=2)
if __name__ == "__main__":
project_id = "drawingpdfocr-461103"
location = "us"
processor_id = "b838676d4e3b4758"
file_path = "../data/UPLOAD_DOCS/3공구-설계도1-004.pdf"
mime_type = "application/pdf"
try:
document_result = process_document_sample(
project_id=project_id,
location=location,
processor_id=processor_id,
file_path=file_path,
mime_type=mime_type,
field_mask="text,entities", # entities 정보를 받아오도록 field_mask 설정
)
if document_result:
json_output_string = extract_and_convert_to_json(document_result)
print(json_output_string) # 변환된 JSON 문자열을 출력
except FileNotFoundError:
print(
f"오류: 파일 경로 '{file_path}'에서 파일을 찾을 수 없습니다. 파일 경로를 확인해주세요."
)
except Exception as e:
print(f"함수 실행 중 오류 발생: {e}")

16
workspace/utils/config.py Normal file
View File

@@ -0,0 +1,16 @@
# config.py
import os
from pathlib import Path
# 디렉토리 설정
UPLOAD_DOCS_DIR = Path(os.getenv("AUDIO_DIR", "./data/UPLOAD_DOCS"))
RESULT_DIR = Path(os.getenv("RESULT_DIR", "./data/results"))
# 허용 파일 확장자
ALLOWED_EXTENSIONS = {".pdf"}
# CORS 설정
CORS_ALLOW_ORIGINS = os.getenv("CORS_ALLOW_ORIGINS", "*").split(",")
CORS_ALLOW_CREDENTIALS = os.getenv("CORS_ALLOW_CREDENTIALS", "true").lower() == "true"
CORS_ALLOW_METHODS = os.getenv("CORS_ALLOW_METHODS", "*").split(",")
CORS_ALLOW_HEADERS = os.getenv("CORS_ALLOW_HEADERS", "*").split(",")

View File

@@ -0,0 +1,58 @@
# utils/file_utils.py
from pathlib import Path
from fastapi import HTTPException, UploadFile
from snowflake import SnowflakeGenerator
from utils.config import ALLOWED_EXTENSIONS, RESULT_DIR, UPLOAD_DOCS_DIR
def create_essential_directories():
"""애플리케이션 시작 시 필요한 디렉토리를 생성합니다."""
UPLOAD_DOCS_DIR.mkdir(exist_ok=True)
RESULT_DIR.mkdir(exist_ok=True)
def create_key(node=1):
generator = SnowflakeGenerator(node)
key_value = next(generator)
return str(key_value)
def save_uploaded_file(
upload_file: UploadFile, save_dir: Path, file_prefix: str
) -> tuple[str, bytes]:
"""
업로드된 파일을 지정된 디렉토리에 저장하고, 파일 내용을 바이트로 반환합니다.
Returns:
저장된 파일 경로 (str), 파일 내용 (bytes)
"""
file_extension = Path(upload_file.filename).suffix.lower()
if file_extension not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"지원하지 않는 파일 형식이에요. 지원 형식: {', '.join(ALLOWED_EXTENSIONS)} 😢",
)
new_filename = f"{file_prefix}{file_extension}"
file_path = save_dir / new_filename
try:
upload_file.file.seek(0)
file_content = upload_file.file.read() # 내용을 읽음
with open(file_path, "wb") as buffer:
buffer.write(file_content) # 내용을 저장
print(f"File saved: {file_path}")
return str(file_path), file_content
except IOError as e:
print(f"File saving error for {file_prefix}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"파일 저장 중 오류 발생: {str(e)}")
except Exception as e:
print(f"Unexpected file saving error for {file_prefix}: {e}", exc_info=True)
raise HTTPException(
status_code=500, detail=f"파일 처리 중 예상치 못한 오류 발생: {str(e)}"
)