원 레포랑 완전 분리

This commit is contained in:
ai-cell-a100-1
2025-08-11 18:56:38 +09:00
commit 7217d3cbaa
86 changed files with 6631 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
from .api_key_router import router as api_key_router
from .download_router import router as download_router
from .dummy_router import router as dummy_router
from .extract_router import router as extract_router
from .general_router import router as general_router
from .guide_router import router as guide_router
from .llm_summation import router as llm_summation
from .model_router import router as model_router
from .ocr_router import router as ocr_router
from .stt_router import router as stt_router
from .yolo_router import router as yolo_router
__all__ = [
"api_key_router",
"download_router",
"dummy_router",
"extract_router",
"general_router",
"guide_router",
"model_router",
"ocr_router",
"stt_router",
"llm_summation",
"yolo_router",
]

View File

@@ -0,0 +1,42 @@
from fastapi import APIRouter, Body, HTTPException
from services import api_key_service
router = APIRouter(prefix="/manage", tags=["API Key Management"])
@router.post("/keys", summary="Create a new API Key")
def create_key(
client_name: str = Body(
...,
embed=True,
description="Name of the client or service that will use this key.",
),
):
"""
새로운 API 키를 생성하고 시스템에 등록합니다.
"""
if not client_name:
raise HTTPException(status_code=400, detail="Client name is required.")
new_key_info = api_key_service.create_api_key(client_name)
return {"message": "API Key created successfully", "key_info": new_key_info}
@router.get("/keys", summary="List all API Keys")
def list_keys():
"""
현재 시스템에 등록된 모든 API 키의 정보를 조회합니다.
"""
keys = api_key_service.list_api_keys()
return {"keys": keys}
@router.delete("/keys/{api_key}", summary="Revoke an API Key")
def revoke_key(api_key: str):
"""
지정된 API 키를 시스템에서 영구적으로 삭제(폐기)합니다.
"""
success = api_key_service.revoke_api_key(api_key)
if not success:
raise HTTPException(status_code=404, detail="API Key not found.")
return {"message": f"API Key '{api_key}' has been revoked."}

View File

@@ -0,0 +1,22 @@
from fastapi import APIRouter
from services.download_service import DownloadService
router = APIRouter(tags=["Model Management"])
# ✅ GET:기본 프롬프트 다운로드
@router.get("/default_prompt", summary="기본 프롬프트 파일 다운로드")
async def download_default_prompt():
return DownloadService.download_default_prompt()
# ✅ GET:구조화 프롬프트 파일 다운로드
@router.get("/structured_prompt", summary="구조화 프롬프트 파일 다운로드")
async def download_structured_prompt():
return DownloadService.download_structured_prompt()
# ✅ GET:구조화 필드 정의 파일 다운로드
@router.get("/structured_schema", summary="구조화 포맷 정의 파일 다운로드")
async def download_structured_schema():
return DownloadService.download_structured_schema()

View File

@@ -0,0 +1,68 @@
from typing import Optional
from fastapi import APIRouter, Depends, Form, Request
from services.inference_service import InferenceHandler
from utils.checking_keys import get_api_key
router = APIRouter(prefix="/dummy", tags=["Dummy"])
# ✅ POST:DUMMY
@router.post(
"/extract/outer",
summary="더미 응답 생성",
description="""### **요약**
실제 모델 추론이나 파일 업로드 없이, 지정된 모델의 응답 형식을 테스트하기 위한 더미(dummy) 결과를 생성합니다.
### **작동 방식**
- 요청 시, 시스템에 미리 저장된 더미 응답(`dummy_response.json`)을 즉시 반환합니다.
- 실제 OCR, LLM 추론 등 어떠한 백그라운드 작업도 수행하지 않습니다.
- 네트워크나 모델 성능에 관계없이 API 응답 구조를 빠르게 확인하는 용도로 사용됩니다.
### **입력 (multipart/form-data)**
- `model` (선택): 응답 형식의 기준이 될 모델 이름. (기본값: `dummy`)
- 이 값은 실제 추론에 사용되지 않으며, 형식 테스트용으로만 기능합니다.
### **출력 (application/json)**
- **즉시 반환**:
```json
{
"filename": "dummy_input.pdf",
"dummy_model": {
"ocr_model": "dummy",
"llm_model": "dummy",
"api_url": "dummy"
},
"time": {
"duration_sec": "0.00",
"started_at": "...",
"ended_at": "..."
},
"fields": {},
"parsed": "dummy",
"generated": "dummy",
"processed": {
"dummy response"
}
}
```
""",
)
async def extract_outer(
request_info: Request,
model: Optional[str] = Form(
default="dummy", description="실제 추론 없이 포맷 테스트용으로 사용됩니다."
),
api_key: str = Depends(get_api_key),
):
return await InferenceHandler.handle_extract_background(
request_id=None,
result_id=None,
input_file=None,
schema_file=None,
prompt_file=None,
mode="dummy",
model_list=[model],
request_info=request_info,
api_key=api_key,
)

View File

@@ -0,0 +1,728 @@
import asyncio
import io
import json
from typing import Optional
from urllib.parse import urlparse
import requests
from config.setting import (
D6C_PROMPT_PATH,
I18N_PROMPT_PATH,
PGN_REDIS_DB,
PGN_REDIS_HOST,
PGN_REDIS_PORT,
)
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
from fastapi.responses import JSONResponse
from redis import Redis
from services.inference_service import InferenceHandler
from utils.checking_files import (
clone_upload_file,
validate_all_files,
)
from utils.checking_keys import create_key, get_api_key
from utils.minio_utils import fetch_result_from_minio
# Redis 클라이언트 (LLM Gateway 전용)
redis_client = Redis(
host=PGN_REDIS_HOST, port=PGN_REDIS_PORT, db=PGN_REDIS_DB, decode_responses=True
)
router = APIRouter(prefix="/extract", tags=["Extraction"])
# ✅ 공통 비동기 추론 엔드포인트 생성기
def register_extract_route(
path: str, mode: str, default_model: str, summary: str, description: str
):
@router.post(path, summary=summary, description=description)
async def extract_endpoint(
request_info: Request,
input_file: UploadFile = File(...),
prompt_file: Optional[UploadFile] = File(
default=None,
description="⚠️ prompt_file 업로드하지 않을 경우, **'Send empty value'** 체크박스를 반드시 해제해주세요.",
),
model: Optional[str] = Form(default=default_model),
api_key: str = Depends(get_api_key),
):
validate_all_files(input_file)
# ✅ 고유한 요청 ID 생성
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
# ✅ 백그라운드에서 작업 실행
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=None,
prompt_file=cloned_prompt,
mode=mode,
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
# ✅ request_id → result_id 매핑 저장
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
# FastAPI 문서화용 정보 부여
extract_endpoint.__name__ = f"extract_{mode}"
extract_endpoint.__doc__ = description
return extract_endpoint
# ✅ 내부 모델용 등록
extract_inner = register_extract_route(
path="/inner",
mode="inner",
default_model="gemma3:27b",
summary="내부 LLM 기반 문서 정보 추출 (비동기)",
description="""### **요약**
내부망에 배포된 LLM(Ollama 기반)을 사용하여 문서(PDF, 이미지 등)에서 정보를 추출하고 응답을 생성합니다. 이 엔드포인트는 사전 정의된 기본 프롬프트를 사용하며, 비동기적으로 처리됩니다.
### **작동 방식**
1. **요청 접수**: `input_file`을 받아 고유 `request_id`를 생성하고 즉시 반환합니다.
2. **백그라운드 처리**:
- `input_file`에 대해 **OCR API**를 호출하여 텍스트를 추출합니다.
- 시스템에 내장된 기본 프롬프트와 추출된 텍스트를 조합합니다. (`prompt_file`을 업로드하여 기본 프롬프트를 대체할 수 있습니다.)
- 내부 LLM(Ollama)에 추론을 요청합니다.
3. **상태 및 결과 확인**: `GET /extract/progress/{request_id}`로 작업 상태와 최종 결과를 조회합니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 정보 추출의 대상이 될 문서 파일.
- 지원 형식: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` 등.
- `prompt_file` (선택): 기본 프롬프트 대신 사용할 사용자 정의 `.txt` 프롬프트 파일.
- `model` (선택): 사용할 내부 LLM 모델 이름. (기본값: `gemma3:27b`)
### **출력 (application/json)**
- **초기 응답**:
```json
{
"message": "작업이 백그라운드에서 실행 중입니다.",
"request_id": "고유한 요청 ID",
"status_check_url": "/extract/progress/고유한 요청 ID"
}
```
- **최종 결과**: `GET /extract/progress/{request_id}`를 통해 확인 가능.
""",
)
# ✅ 외부 모델용 등록
extract_outer = register_extract_route(
path="/outer",
mode="outer",
default_model="gemini-2.5-flash",
summary="외부 LLM 기반 문서 정보 추출 (비동기)",
description="""### **요약**
외부 상용 LLM(예: GPT, Gemini)을 사용하여 문서에서 정보를 추출하고 응답을 생성합니다. 내부 LLM 엔드포인트와 작동 방식은 동일하나, 외부 API를 호출합니다.
### **작동 방식**
1. **요청 접수**: `input_file`을 받아 `request_id`를 생성 후 즉시 반환합니다.
2. **백그라운드 처리**:
- `input_file`에서 **OCR API**를 통해 텍스트를 추출합니다.
- 내장된 기본 프롬프트(또는 사용자 정의 `prompt_file`)와 텍스트를 조합합니다.
- 외부 LLM API(OpenAI, Google 등)에 추론을 요청합니다.
3. **상태 및 결과 확인**: `GET /extract/progress/{request_id}`로 작업 상태와 최종 결과를 조회합니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
- `prompt_file` (선택): 기본 프롬프트 대신 사용할 `.txt` 파일.
- `model` (선택): 사용할 외부 LLM 모델 이름. (기본값: `gemini-2.5-flash`)
### **출력 (application/json)**
- **초기 응답**:
```json
{
"message": "작업이 백그라운드에서 실행 중입니다.",
"request_id": "고유한 요청 ID",
"status_check_url": "/extract/progress/고유한 요청 ID"
}
```
- **최종 결과**: `GET /extract/progress/{request_id}`를 통해 확인 가능.
""",
)
# ✅ 멀티모달 GPT 테스트용 등록
extract_outer_gpt = register_extract_route(
path="/outer/gpt",
mode="multimodal",
default_model="gpt-4o",
summary="멀티모달 GPT 테스트용",
description="""### **요약**
GPT-4o와 같은 멀티모달 모델을 사용하여, 문서(PDF, 이미지 등)에서 정보를 추출하고 응답을 생성합니다.
### **작동 방식**
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
- 추론 단계에서 시스템은 멀티모달 출력을 생성하도록 특화된 프롬프트(`multimodal_prompt`)를 사용합니다.
- LLM은 `input_file`의 내용과 `prompt_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
- `prompt_file` (**선택**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
- `model` (선택): 사용할 LLM 모델 이름.
### **출력 (application/json)**
- **초기 응답**: `request_id` 포함.
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
""",
)
# ✅ 멀티모달 Gemini 테스트용 등록
extract_outer_gemini = register_extract_route(
path="/outer/gemini",
mode="multimodal",
default_model="gemini-2.5-flash",
summary="멀티모달 Gemini 테스트용",
description="""### **요약**
Gemini와 같은 멀티모달 모델을 사용하여, 문서(PDF, 이미지 등)에서 정보를 추출하고 응답을 생성합니다.
### **작동 방식**
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
- 추론 단계에서 시스템은 멀티모달 출력을 생성하도록 특화된 프롬프트(`multimodal_prompt`)를 사용합니다.
- LLM은 `input_file`의 내용과 `prompt_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
- `prompt_file` (**선택**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
- `model` (선택): 사용할 LLM 모델 이름.
### **출력 (application/json)**
- **초기 응답**: `request_id` 포함.
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
""",
)
@router.post(
"/inner/d6c",
summary="국내 문서 테스트용",
)
async def extract_d6c(
request_info: Request,
input_file: UploadFile = File(...),
model: Optional[str] = Form(default="gemma3:27b"),
api_key: str = Depends(get_api_key),
):
validate_all_files(input_file)
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
with open(I18N_PROMPT_PATH, "rb") as f:
content = f.read()
# 메모리 내 파일 객체 생성
spooled_file = io.BytesIO(content)
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
dummy_prompt_file = UploadFile(filename=I18N_PROMPT_PATH.name, file=spooled_file)
cloned_prompt = clone_upload_file(dummy_prompt_file)
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=None,
prompt_file=cloned_prompt,
mode="inner",
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
@router.post(
"/inner/i18n",
summary="해외 문서 테스트용",
)
async def extract_i18n(
request_info: Request,
input_file: UploadFile = File(...),
model: Optional[str] = Form(default="gemma3:27b"),
api_key: str = Depends(get_api_key),
):
validate_all_files(input_file)
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
with open(D6C_PROMPT_PATH, "rb") as f:
content = f.read()
# 메모리 내 파일 객체 생성
spooled_file = io.BytesIO(content)
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
dummy_prompt_file = UploadFile(filename=D6C_PROMPT_PATH.name, file=spooled_file)
cloned_prompt = clone_upload_file(dummy_prompt_file)
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=None,
prompt_file=cloned_prompt,
mode="inner",
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
# ✅ structured 모드: 구조화 JSON 스키마 기반 추론
@router.post(
"/inner/structured",
summary="구조화된 JSON 정보 추출 (비동기)",
description="""### **요약**
사용자가 제공한 `schema_file`에 정의된 JSON 스키마에 따라, 문서에서 정보를 추출하여 구조화된 JSON으로 반환합니다.
### **작동 방식**
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
- 추론 단계에서 시스템은 구조화된 출력을 생성하도록 특화된 프롬프트(`structured_prompt`)를 사용합니다.
- LLM은 `input_file`의 내용과 `schema_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
- `schema_file` (**필수**): 원하는 출력 JSON 구조를 정의하는 `.json` 파일.
- `prompt_file` (**필수**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
- `model` (선택): 사용할 LLM 모델 이름.
### **출력 (application/json)**
- **초기 응답**: `request_id` 포함.
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
""",
)
async def extract_structured_inner(
request_info: Request,
input_file: UploadFile = File(...),
schema_file: UploadFile = File(...),
prompt_file: UploadFile = File(...),
model: Optional[str] = Form(default="gemma3:27b"),
api_key: str = Depends(get_api_key),
):
validate_all_files(input_file)
# ✅ 고유한 요청 ID 생성
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
cloned_schema = clone_upload_file(schema_file) if schema_file else None
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
# ✅ 백그라운드에서 작업 실행
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=cloned_schema,
prompt_file=cloned_prompt,
mode="structured",
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
# ✅ request_id → result_id 매핑 저장
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
# ✅ structured 모드: 구조화 JSON 스키마 기반 추론
@router.post(
"/outer/structured",
summary="구조화된 JSON 정보 추출 (비동기)",
description="""### **요약**
사용자가 제공한 `schema_file`에 정의된 JSON 스키마에 따라, 문서에서 정보를 추출하여 구조화된 JSON으로 반환합니다.
### **작동 방식**
- 다른 추출 엔드포인트와 동일한 비동기 파이프라인을 따릅니다.
- 추론 단계에서 시스템은 구조화된 출력을 생성하도록 특화된 프롬프트(`structured_prompt`)를 사용합니다.
- LLM은 `input_file`의 내용과 `schema_file`의 구조를 바탕으로 JSON 객체를 생성합니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 정보 추출 대상 문서 파일.
- `schema_file` (**선택**): 원하는 출력 JSON 구조를 정의하는 `.json` 파일.
- `prompt_file` (**선택**): 구조화용 기본 프롬프트 대신 사용할 `.txt` 파일.
- `model` (선택): 사용할 LLM 모델 이름.
### **출력 (application/json)**
- **초기 응답**: `request_id` 포함.
- **최종 결과**: `GET /extract/progress/{request_id}` 조회 시, 지정된 스키마를 따르는 JSON 객체가 반환됩니다.
""",
)
async def extract_structured_outer(
request_info: Request,
input_file: UploadFile = File(...),
schema_file: Optional[UploadFile] = File(
default=None,
description="⚠️ schema_file 업로드하지 않을 경우, **'Send empty value'** 체크박스를 반드시 해제해주세요.",
),
prompt_file: Optional[UploadFile] = File(
default=None,
description="⚠️ prompt_file 업로드하지 않을 경우, **'Send empty value'** 체크박스를 반드시 해제해주세요.",
),
model: Optional[str] = Form(default="gemma3:27b"),
api_key: str = Depends(get_api_key),
):
validate_all_files(input_file)
# ✅ 고유한 요청 ID 생성
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
cloned_schema = clone_upload_file(schema_file) if schema_file else None
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
# ✅ 백그라운드에서 작업 실행
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=cloned_schema,
prompt_file=cloned_prompt,
mode="structured",
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
# ✅ request_id → result_id 매핑 저장
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
# ✅ 상태 로그 조회 API
@router.get(
"/progress/{request_id}",
summary="정보 추출 작업 상태 및 결과 조회",
description="""### **요약**
`POST /extract/*` 계열 엔드포인트 요청 시 반환된 `request_id`를 사용하여, 해당 정보 추출 작업의 진행 상태와 최종 결과를 조회합니다.
### **작동 방식**
- `request_id`를 기반으로 Redis에 저장된 작업 로그와 결과 데이터를 조회합니다.
- 작업이 진행 중일 때는 현재까지의 로그를, 완료되었을 때는 로그와 함께 최종 결과(`final_result`)를 반환합니다.
### **입력**
- `request_id`: 조회할 작업의 고유 ID.
### **출력 (application/json)**
- **성공 시**:
```json
{
"request_id": "요청 시 사용된 ID",
"progress_logs": [
{ "timestamp": "...", "status": "OCR 시작", "details": "..." },
{ "timestamp": "...", "status": "입력 길이 검사 시작", "details": "..." },
{ "timestamp": "...", "status": "LLM 추론 시작", "details": "..." },
{ "timestamp": "...", "status": "LLM 추론 완료 및 후처리 시작", "details": "..." },
{ "timestamp": "...", "status": "후처리 완료 및 결과 반환"", "details": "..." }
],
"final_result": {
"filename": "입력 파일",
"processed": "LLM의 최종 응답 내용"
}
}
```
- **ID가 유효하지 않을 경우 (404 Not Found)**:
```json
{
"message": "{request_id}에 대한 상태 로그가 없습니다."
}
```
""",
)
async def get_pipeline_status(request_id: str):
redis_key = f"pipeline_status:{request_id}"
# 1. 상태 로그 조회
logs = redis_client.lrange(redis_key, 0, -1)
if not logs:
return JSONResponse(
status_code=404,
content={"message": f"{request_id}에 대한 상태 로그가 없습니다."},
)
parsed_logs = [json.loads(log) for log in logs] if logs else []
# 2. request_id → result_id 매핑 조회
result_id = redis_client.hget("pipeline_result_mapping", request_id)
final_result = None
if result_id:
# 3. Redis에서 최종 결과 조회
result_key = f"pipeline_result:{result_id}"
result_str = redis_client.get(result_key)
if result_str:
try:
final_result = json.loads(result_str)
return JSONResponse(
content={
"request_id": request_id,
"progress_logs": parsed_logs,
"final_result": final_result,
}
)
except json.JSONDecodeError:
final_result = {
"massage": "[REDIS] 결과 존재하지만, 디코딩에 실패했습니다."
}
else:
print(f"[REDIS] request_id {request_id} 가 Redis에 없습니다.")
# 4. Redis에 결과가 없으면 MinIO에서 조회
try:
print(f"[MINIO] MinIO에서 결과를 가져오는 중: {request_id}")
final_result = fetch_result_from_minio(request_id)
if final_result:
return JSONResponse(
content={
"request_id": request_id,
"progress_logs": parsed_logs,
"final_result": final_result,
}
)
else:
# MinIO에서 결과가 없으면 작업 진행 상태 실시간 확인
return JSONResponse(
content={
"request_id": request_id,
"progress_logs": parsed_logs,
"final_result": "작업이 진행 중입니다. 결과는 아직 생성되지 않았습니다.",
}
)
except Exception as e:
print(f"[MINIO] MinIO 결과 조회 중 실패했습니다: {e}")
## 조찬영
@router.post(
"/inner2/d6c",
summary="국내 문서 테스트용",
)
async def extract2_d6c(
request_info: Request,
minio_url: str = Form(...),
model: Optional[str] = Form(default="qwen3:30b"),
api_key: str = Depends(get_api_key),
):
try:
response = requests.get(minio_url)
response.raise_for_status() # 4xx/5xx 응답에 대해 HTTPError 발생
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
return JSONResponse(
status_code=400,
content={
"message": "제공된 MinIO URL이 만료되었거나 접근 권한이 없습니다."
},
)
else:
return JSONResponse(
status_code=400,
content={
"message": f"URL에서 파일을 가져오는 데 실패했습니다: {e.response.status_code} {e.response.reason}"
},
)
except requests.exceptions.RequestException as e:
return JSONResponse(
status_code=400,
content={"message": f"URL에 연결하는 중 오류가 발생했습니다: {e}"},
)
# URL에서 쿼리 파라미터를 제외한 파일 이름 추출
parsed_url = urlparse(minio_url)
file_name = parsed_url.path.split("/")[-1]
# 다운로드한 파일 데이터로 UploadFile 객체 생성
input_file = UploadFile(filename=file_name, file=io.BytesIO(response.content))
validate_all_files(input_file)
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
with open(I18N_PROMPT_PATH, "rb") as f:
content = f.read()
# 메모리 내 파일 객체 생성
spooled_file = io.BytesIO(content)
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
dummy_prompt_file = UploadFile(filename=I18N_PROMPT_PATH.name, file=spooled_file)
cloned_prompt = clone_upload_file(dummy_prompt_file)
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=None,
prompt_file=cloned_prompt,
mode="inner",
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
@router.post(
"/inner2/i18n",
summary="해외 문서 테스트용",
)
async def extract2_i18n(
request_info: Request,
minio_url: str = Form(...),
model: Optional[str] = Form(default="qwen3:30b"),
api_key: str = Depends(get_api_key),
):
try:
response = requests.get(minio_url)
response.raise_for_status() # 4xx/5xx 응답에 대해 HTTPError 발생
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
return JSONResponse(
status_code=400,
content={
"message": "제공된 MinIO URL이 만료되었거나 접근 권한이 없습니다."
},
)
else:
return JSONResponse(
status_code=400,
content={
"message": f"URL에서 파일을 가져오는 데 실패했습니다: {e.response.status_code} {e.response.reason}"
},
)
except requests.exceptions.RequestException as e:
return JSONResponse(
status_code=400,
content={"message": f"URL에 연결하는 중 오류가 발생했습니다: {e}"},
)
# URL에서 쿼리 파라미터를 제외한 파일 이름 추출
parsed_url = urlparse(minio_url)
file_name = parsed_url.path.split("/")[-1]
# 다운로드한 파일 데이터로 UploadFile 객체 생성
input_file = UploadFile(filename=file_name, file=io.BytesIO(response.content))
validate_all_files(input_file)
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
# 설정에 정의된 기본 I18N 프롬프트 파일을 항상 사용
with open(D6C_PROMPT_PATH, "rb") as f:
content = f.read()
# 메모리 내 파일 객체 생성
spooled_file = io.BytesIO(content)
# UploadFile과 유사한 객체를 생성하여 백그라운드 핸들러로 전달
dummy_prompt_file = UploadFile(filename=D6C_PROMPT_PATH.name, file=spooled_file)
cloned_prompt = clone_upload_file(dummy_prompt_file)
asyncio.create_task(
InferenceHandler.handle_extract_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=None,
prompt_file=cloned_prompt,
mode="inner",
model_list=[model],
request_info=request_info,
api_key=api_key,
)
)
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/extract/progress/{request_id}",
}
)
## 조찬영

View File

@@ -0,0 +1,235 @@
import asyncio
import json
from typing import Optional
from config.setting import (
PGN_REDIS_DB,
PGN_REDIS_HOST,
PGN_REDIS_PORT,
)
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
from fastapi.responses import JSONResponse
from redis import Redis
from services.inference_service import InferenceHandler
from utils.checking_files import (
clone_upload_file,
validate_all_files,
)
from utils.checking_keys import create_key, get_api_key
# Redis 클라이언트 (LLM Gateway 전용)
redis_client = Redis(
host=PGN_REDIS_HOST, port=PGN_REDIS_PORT, db=PGN_REDIS_DB, decode_responses=True
)
router = APIRouter(prefix="/general", tags=["General"])
# ✅ 공통 비동기 추론 엔드포인트 생성기
def register_general_route(
path: str, mode: str, default_model: str, summary: str, description: str
):
@router.post(path, summary=summary, description=description)
async def general_endpoint(
request_info: Request,
input_file: UploadFile = File(...),
prompt_file: UploadFile = File(...),
schema_file: Optional[UploadFile] = File(default=None),
model: Optional[str] = Form(default=default_model),
api_key: str = Depends(get_api_key),
):
validate_all_files(input_file)
# ✅ 고유한 요청 ID 생성
request_id = create_key()
result_id = create_key()
cloned_input = clone_upload_file(input_file) if input_file else None
cloned_prompt = clone_upload_file(prompt_file) if prompt_file else None
cloned_schema = clone_upload_file(schema_file) if schema_file else None
effective_mode = "structured" if schema_file and schema_file.filename else mode
# ✅ 백그라운드에서 작업 실행
asyncio.create_task(
InferenceHandler.handle_general_background(
request_id=request_id,
result_id=result_id,
input_file=cloned_input,
schema_file=cloned_schema,
prompt_file=cloned_prompt,
mode=effective_mode,
model=model,
request_info=request_info,
api_key=api_key,
)
)
# ✅ request_id → result_id 매핑 저장
redis_client.hset("pipeline_result_mapping", request_id, result_id)
return JSONResponse(
content={
"message": "문서 추출 및 생성형 응답 작업이 백그라운드에서 실행 중입니다.",
"request_id": request_id,
"status_check_url": f"/general/progress/{request_id}",
}
)
# FastAPI 문서화용 정보 부여
general_endpoint.__name__ = f"general_{mode}"
general_endpoint.__doc__ = description
return general_endpoint
# ✅ 내부 모델용 등록
general_inner = register_general_route(
path="/inner",
mode="inner",
default_model="gemma3:27b",
summary="내부 LLM 기반 범용 추론 요청 (비동기)",
description="""### **요약**
내부망에 배포된 LLM(Ollama 기반)을 사용하여 문서 기반의 범용 추론을 비동기적으로 요청합니다. 이 엔드포인트는 파일(PDF, 이미지 등)에서 텍스트를 추출하고, 사용자가 제공한 프롬프트를 적용하여 결과를 생성합니다.
### **작동 방식**
1. **요청 접수**: `input_file`, `prompt_file` 등을 받아 고유한 `request_id`를 생성하고 즉시 반환합니다.
2. **백그라운드 처리**:
- `input_file`이 문서나 이미지일 경우, **OCR API**를 호출하여 텍스트를 추출합니다.
- 추출된 텍스트와 `prompt_file`의 내용을 조합하여 최종 프롬프트를 구성합니다.
- 내부 LLM(Ollama)에 추론을 요청합니다.
- `schema_file`이 제공되면, LLM이 스키마에 맞는 JSON을 생성하도록 요청합니다.
3. **상태 및 결과 확인**: 반환된 `request_id`를 사용하여 `GET /general/progress/{request_id}` 엔드포인트에서 작업 진행 상태와 최종 결과를 조회할 수 있습니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 추론의 기반이 될 문서 파일.
- 지원 형식: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` 등.
- 내부적으로 OCR을 통해 텍스트가 자동 추출됩니다.
- `prompt_file` (**필수**): LLM에 전달할 명령어(프롬프트)가 포함된 `.txt` 파일.
- `schema_file` (선택): 결과물의 구조를 정의하는 `.json` 스키마 파일. 제공 시, 출력은 이 스키마를 따르는 JSON 형식으로 강제됩니다.
- `model` (선택): 사용할 내부 LLM 모델 이름. (기본값: `gemma3:27b`)
### **출력 (application/json)**
- **초기 응답**:
```json
{
"message": "작업이 백그라운드에서 실행 중입니다.",
"request_id": "고유한 요청 ID",
"status_check_url": "/general/progress/고유한 요청 ID"
}
```
- **최종 결과**: `GET /general/progress/{request_id}`를 통해 확인 가능.
""",
)
# ✅ 외부 모델용 등록
general_outer = register_general_route(
path="/outer",
mode="outer",
default_model="gemini-2.5-flash",
summary="외부 LLM 기반 범용 추론 요청 (비동기)",
description="""### **요약**
외부 상용 LLM(예: GPT, Gemini, Claude)을 사용하여 문서 기반의 범용 추론을 비동기적으로 요청합니다. 기능과 작동 방식은 내부 LLM용 엔드포인트와 동일하나, 외부 API를 호출하는 점이 다릅니다.
### **작동 방식**
1. **요청 접수**: `input_file`, `prompt_file` 등을 받아 고유한 `request_id`를 생성하고 즉시 반환합니다.
2. **백그라운드 처리**:
- `input_file`에서 **OCR API**를 통해 텍스트를 추출합니다.
- 추출된 텍스트와 `prompt_file`의 내용을 조합하여 최종 프롬프트를 구성합니다.
- 외부 LLM API(OpenAI, Google, Anthropic 등)에 추론을 요청합니다.
- `schema_file`이 제공되면, LLM이 스키마에 맞는 JSON을 생성하도록 요청합니다.
3. **상태 및 결과 확인**: 반환된 `request_id`를 사용하여 `GET /general/progress/{request_id}` 엔드포인트에서 작업 진행 상태와 최종 결과를 조회할 수 있습니다.
### **입력 (multipart/form-data)**
- `input_file` (**필수**): 추론의 기반이 될 문서 파일.
- 지원 형식: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` 등.
- `prompt_file` (**필수**): LLM에 전달할 프롬프트가 포함된 `.txt` 파일.
- `schema_file` (선택): 결과물의 구조를 정의하는 `.json` 스키마 파일.
- `model` (선택): 사용할 외부 LLM 모델 이름. (기본값: `gemini-2.5-flash`)
### **출력 (application/json)**
- **초기 응답**:
```json
{
"message": "작업이 백그라운드에서 실행 중입니다.",
"request_id": "고유한 요청 ID",
"status_check_url": "/general/progress/고유한 요청 ID"
}
```
- **최종 결과**: `GET /general/progress/{request_id}`를 통해 확인 가능.
""",
)
# ✅ 상태 로그 조회 API
@router.get(
"/progress/{request_id}",
summary="범용 추론 작업 상태 및 결과 조회",
description="""### **요약**
`POST /general/inner` 또는 `POST /general/outer` 요청 시 반환된 `request_id`를 사용하여, 해당 작업의 진행 상태와 최종 결과를 조회합니다.
### **작동 방식**
- `request_id`를 기반으로 Redis에 저장된 작업 로그와 결과 데이터를 조회합니다.
- 작업이 진행 중일 때는 현재까지의 로그를, 완료되었을 때는 로그와 함께 최종 결과(`final_result`)를 반환합니다.
### **입력**
- `request_id`: 조회할 작업의 고유 ID.
### **출력 (application/json)**
- **성공 시**:
```json
{
"request_id": "요청 시 사용된 ID",
"progress_logs": [
{ "timestamp": "...", "status": "OCR 시작", "details": "..." },
{ "timestamp": "...", "status": "입력 길이 검사 시작", "details": "..." },
{ "timestamp": "...", "status": "LLM 추론 시작", "details": "..." },
{ "timestamp": "...", "status": "LLM 추론 완료 및 후처리 시작", "details": "..." },
{ "timestamp": "...", "status": "후처리 완료 및 결과 반환"", "details": "..." }
],
"final_result": {
"filename": "입력 파일",
"processed": "LLM의 최종 응답 내용"
}
}
```
- **ID가 유효하지 않을 경우 (404 Not Found)**:
```json
{
"message": "{request_id}에 대한 상태 로그가 없습니다."
}
```
""",
)
async def get_pipeline_status(request_id: str):
# 상태 로그 조회
redis_key = f"pipeline_status:{request_id}"
logs = redis_client.lrange(redis_key, 0, -1)
if not logs:
return JSONResponse(
status_code=404,
content={"message": f"{request_id}에 대한 상태 로그가 없습니다."},
)
parsed_logs = [json.loads(log) for log in logs] if logs else []
# request_id → result_id 매핑 조회
result_id = redis_client.hget("pipeline_result_mapping", request_id)
final_result = None
if result_id:
# 최종 결과 조회
result_key = f"pipeline_result:{result_id}"
result_str = redis_client.get(result_key)
if result_str:
try:
final_result = json.loads(result_str)
except json.JSONDecodeError:
final_result = {"error": "결과 디코딩 실패"}
return JSONResponse(
content={
"request_id": request_id,
"progress_logs": parsed_logs,
"final_result": final_result,
}
)

View File

@@ -0,0 +1,46 @@
from config.setting import (
EXTRACT_DEFAULT_PATH,
GENERAL_GUIDE_PATH,
SCHEMA_FILE_PATH,
)
from fastapi import APIRouter
from fastapi.responses import FileResponse, HTMLResponse
router = APIRouter(tags=["Guide Book"])
# ✅ /schema_json 가이드 HTML
@router.get(
"/schema_file_guide",
summary="schema 파일 작성 가이드북 HTML 보기",
description=(
"📄 본 가이드북은 <strong>/general</strong> 및 <strong>/extract/structured</strong> "
"엔드포인트에 첨부되는 <strong>schema_file</strong> 작성법을 설명합니다.<br><br>"
"가이드북은 <a href='/schema_file_guide' target='_blank'>여기</a>에서 확인하세요."
),
response_class=HTMLResponse,
)
async def schema_guide():
return FileResponse(SCHEMA_FILE_PATH, media_type="text/html")
# ✅ /general 가이드 HTML
@router.get(
"/general_guide",
summary="/general 가이드북 HTML 보기",
description="가이드북을 <a href='/general_guide' target='_blank'>여기</a>에서 확인하세요.",
response_class=HTMLResponse,
)
async def general_guide():
return FileResponse(GENERAL_GUIDE_PATH, media_type="text/html")
# ✅ /extract 가이드 HTML
@router.get(
"/extract_guide",
summary="/extract 가이드북 HTML 보기",
description="가이드북을 <a href='/extract_guide' target='_blank'>여기</a>에서 확인하세요.",
response_class=HTMLResponse,
)
async def extract_guide():
return FileResponse(EXTRACT_DEFAULT_PATH, media_type="text/html")

View File

@@ -0,0 +1,86 @@
import logging
from fastapi import APIRouter, BackgroundTasks, Depends
from pydantic import BaseModel
from services.report import (
ask_ollama_qwen,
dialog_ask_gemini,
run_all_models,
tasks_store,
total_summation,
)
from utils.checking_keys import create_key
from utils.logging_utils import EndpointLogger
# ------------------------------------------
# 로깅 설정
logger = logging.getLogger(__name__)
router = APIRouter(tags=["summary"])
class SummaryRequest(BaseModel):
text: str
@router.post("/summary") # STT 요약 모델
async def summarize(
request: SummaryRequest, endpoint_logger: EndpointLogger = Depends(EndpointLogger)
):
endpoint_logger.log(
model="gpt-4.1-mini, qwen3:custom, gemini-2.5-flash, claude-3-7-sonnet-latest",
input_filename="None",
prompt_filename="None",
context_length=len(request.text),
)
results = await total_summation(request.text)
return {"summary_results": results}
@router.post("/ollama_summary") # ollama 모델 전용
async def ollama_summary(
request: SummaryRequest, endpoint_logger: EndpointLogger = Depends(EndpointLogger)
):
endpoint_logger.log(
model="qwen3:custom",
input_filename="None",
prompt_filename="None",
context_length=len(request.text),
)
results = await ask_ollama_qwen(request.text)
return {"summary_results": results}
@router.post("/gemini_summary")
async def gemini_summary(request: SummaryRequest):
results = await dialog_ask_gemini(request.text)
return {"summary_results": results}
@router.post("/task_summary") # 모델 별 전체 요약
async def task_summary(
request: SummaryRequest,
background_tasks: BackgroundTasks,
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
):
endpoint_logger.log(
model="gpt-4.1-mini, qwen3:custom, gemini-2.5-flash, claude-3-7-sonnet-latest",
input_filename="None",
prompt_filename="None",
context_length=len(request.text),
)
task_id = create_key()
background_tasks.add_task(run_all_models, request.text, task_id)
return {"task_id": task_id}
@router.get("/task_summary/{task_id}") # 모델 별 요약 조회
async def get_status(task_id: str):
task = tasks_store.get(task_id)
if not task:
return {"error": "Invalid task_id"}
return task

View File

@@ -0,0 +1,17 @@
from fastapi import APIRouter
from services.model_service import ModelInfoService
router = APIRouter(tags=["Model Management"])
# ✅ GET:사용 가능한 모델 조회 API
@router.get(
"/info",
summary="'/extract', '/general' 에서 사용 가능한 모델 목록 확인",
description="""
'inner(내부용)''outer(외부용)' 모델의 사용 가능한 목록을 확인합니다.<br>
'Try it out''Execute' 순서로 클릭합니다.<br>
""",
)
async def get_model_info():
return await ModelInfoService.get_model_info()

View File

@@ -0,0 +1,168 @@
import logging
import httpx
from config.setting import (
MINIO_BUCKET_NAME,
OCR_API_URL,
)
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from utils.checking_files import validate_all_files
from utils.checking_keys import create_key
from utils.logging_utils import EndpointLogger
from utils.minio_utils import upload_file_to_minio_v2 # ✅ MinIO 유틸 함수 import
router = APIRouter(prefix="/ocr", tags=["OCR"])
logger = logging.getLogger(__name__)
@router.post(
"",
summary="문서 OCR 요청 (비동기)",
description="""### **요약**
문서 파일(PDF, 이미지 등)을 받아 텍스트를 추출하는 OCR(광학 문자 인식) 작업을 비동기적으로 요청합니다.
### **작동 방식**
1. **요청 접수**: `file`을 받아 고유 `request_id`를 생성하고 즉시 반환합니다.
2. **백그라운드 처리**:
- 업로드된 파일을 내부 저장소(MinIO)에 저장합니다.
- 별도의 OCR 서버에 텍스트 추출 작업을 요청합니다.
3. **상태 및 결과 확인**: 반환된 `request_id`를 사용하여 `GET /ocr/progress/{request_id}`로 작업 상태를, `GET /ocr/result/{request_id}`로 최종 텍스트 결과를 조회할 수 있습니다.
### **입력 (multipart/form-data)**
- `file` (**필수**): 텍스트를 추출할 문서 파일.
- 지원 형식: `.pdf`, `.jpg`, `.png`, `.jpeg` 등 OCR 서버가 지원하는 형식.
### **출력 (application/json)**
- **초기 응답**:
```json
[
{
"request_id": "고유한 요청 ID",
"status": "작업 접수",
"message": "아래 URL을 통해 작업 상태 및 결과를 확인하세요."
}
]
```
- **최종 결과**: `GET /ocr/result/{request_id}`를 통해 확인 가능.
""",
)
async def ocr_only(
file: UploadFile = File(...),
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
):
validate_all_files(file)
results = []
endpoint_logger.log(
model="paddle-ocr",
input_filename=file.filename,
context_length=0, # OCR은 context_length가 필요하지 않음
)
async with httpx.AsyncClient() as client:
# ✅ 1. 고유 ID 생성
request_id = create_key()
bucket_name = MINIO_BUCKET_NAME
object_name = f"{request_id}/{file.filename}"
# ✅ 2. MinIO에 파일 업로드 후 presigned URL 생성
# presigned_url = upload_file_to_minio(file, request_id)
presigned_url = upload_file_to_minio_v2(
file=file, bucket_name=bucket_name, object_name=object_name
)
logger.info(f"[MinIO] ✅ presigned URL 생성 완료: {presigned_url}")
try:
# ✅ 3. OCR API에 presigned URL 전달
resp = await client.post(
OCR_API_URL,
json=[
{
"file_url": presigned_url,
"filename": file.filename,
}
],
timeout=None,
)
resp.raise_for_status()
# except httpx.ReadTimeout:
# logger.error("[OCR] OCR 서버 지연 가능성")
# raise HTTPException(
# status_code=504, detail="OCR 서버 응답이 지연되고 있습니다."
# )
# except httpx.HTTPStatusError as e:
# logger.error(
# f"[OCR] ❌ HTTP 에러 발생: {e.response.status_code} - {e.response.text}"
# )
# raise HTTPException(
# status_code=e.response.status_code, detail="OCR 서버 오류 발생"
# )
except Exception:
logger.exception("[OCR] ❌ 예기치 못한 오류 발생")
raise HTTPException(
status_code=500, detail="OCR 요청 처리 중 내부 오류 발생"
)
# ✅ 4. OCR 응답에서 request_id 추출
for item in resp.json().get("results", []):
ocr_request_id = item.get("request_id")
result_item = {
"request_id": ocr_request_id,
"status": "작업 접수",
"message": "아래 URL을 통해 작업 상태 및 결과를 확인하세요.",
}
results.append(result_item)
return JSONResponse(content=results)
@router.get(
"/progress/{request_id}",
summary="OCR 작업 상태 조회",
description="""### **요약**
`POST /ocr` 요청 시 반환된 `request_id`를 사용하여 OCR 작업의 현재 진행 상태를 조회합니다.
### **작동 방식**
- `request_id`를 OCR 서버에 전달하여 해당 작업의 상태를 가져옵니다.
- 상태는 보통 'PENDING', 'IN_PROGRESS', 'SUCCESS', 'FAILURE' 등으로 표시됩니다.
### **입력**
- `request_id`: 조회할 OCR 작업의 고유 ID.
### **출력 (application/json)**
- **성공 시**:
```json
{
"request_id": "요청 시 사용된 ID",
"progress_logs": [
{ "timestamp": "...", "status": "OCR 시작", "details": "..." },
{ "timestamp": "...", "status": "입력 길이 검사 시작", "details": "..." },
{ "timestamp": "...", "status": "LLM 추론 시작", "details": "..." },
{ "timestamp": "...", "status": "LLM 추론 완료 및 후처리 시작", "details": "..." },
{ "timestamp": "...", "status": "후처리 완료 및 결과 반환"", "details": "..." }
],
"final_result": {
"filename": "입력 파일",
"parsed": "OCR 결과 내용"
}
}
```
- **ID가 유효하지 않을 경우 (404 Not Found)**:
```json
{
"detail": "Meeting ID {request_id} 작업 없음"
}
```
""",
)
async def get_pipeline_status(request_id: str):
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{OCR_API_URL}/progress/{request_id}")
return JSONResponse(content=response.json(), status_code=response.status_code)
except Exception as e:
raise HTTPException(status_code=500, detail=f"OCR 상태 조회 실패: {str(e)}")

View File

@@ -0,0 +1,144 @@
# llmgateway/routers/stt_proxy.py
import logging
import httpx
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from utils.checking_keys import create_key
from utils.logging_utils import EndpointLogger
from utils.minio_utils import upload_file_to_minio_v2
router = APIRouter(tags=["STT Gateway"])
STT_API_BASE_URL = "http://stt_fastapi:8899/ccp" # docker-compose 내 서비스명 기반
MULTI_STT_API_BASE_URL = (
"http://stt_fastapi:8899/dialog" # docker-compose 내 서비스명 기반
)
logger = logging.getLogger(__name__)
# 파일 업로드 → stt_api에 Presigned URL 전달
@router.post("/audio")
async def proxy_audio(
audio_file: UploadFile = File(...),
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
):
request_id = create_key()
bucket_name = "stt-gateway"
object_name = f"{request_id}/{audio_file.filename}"
try:
# upload_file_to_minio_v2는 presigned URL을 반환합니다.
presigned_url = upload_file_to_minio_v2(
file=audio_file,
bucket_name=bucket_name,
object_name=object_name,
)
except Exception as e:
logger.error(f"MinIO upload failed: {e}")
raise HTTPException(status_code=500, detail="File upload to storage failed.")
# 로깅
endpoint_logger.log(model="N/A", input_filename=audio_file.filename)
# stt_fastapi에 Presigned URL 정보 전달
try:
async with httpx.AsyncClient() as client:
payload = {
"file_url": presigned_url,
"language": "ko",
}
response = await client.post(f"{STT_API_BASE_URL}/audio", json=payload)
return JSONResponse(content=response.json(), status_code=response.status_code)
except Exception as e:
raise HTTPException(status_code=500, detail=f"STT API 호출 실패: {str(e)}")
# 상태 조회 → stt_api에 중계 및 오류 로깅
@router.get("/progress/{request_id}")
async def proxy_progress(request_id: str):
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{STT_API_BASE_URL}/progress/{request_id}")
response.raise_for_status() # HTTP 오류 발생 시 예외 처리
# 응답 데이터 확인 및 로깅
data = response.json()
if data.get("celery_status") == "FAILURE":
# 상세 오류 정보를 포함하여 에러 로그 기록
error_details = data.get("progress_logs", [])
logger.error(f"[ERROR] STT task failed for request_id {request_id}. Details: {error_details}")
return JSONResponse(content=data, status_code=response.status_code)
except httpx.HTTPStatusError as e:
logger.error(f"STT progress check failed with status {e.response.status_code} for request_id {request_id}: {e.response.text}")
raise HTTPException(status_code=e.response.status_code, detail=f"STT 상태 조회 실패: {e.response.text}")
except Exception as e:
logger.error(f"An unexpected error occurred while checking STT progress for request_id {request_id}: {e}")
raise HTTPException(status_code=500, detail=f"STT 상태 조회 실패: {str(e)}")
# 다중 입력 회의 → stt_api에 Presigned URL 전달
@router.post("/dialog_processing")
async def proxy_dialog_processing(
audio_file: UploadFile = File(...),
meeting_tag: str = Form(...),
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
):
bucket_name = "stt-gateway"
request_id = create_key()
object_name = f"{meeting_tag}_{request_id}/{audio_file.filename}"
try:
presigned_url = upload_file_to_minio_v2(
file=audio_file,
bucket_name=bucket_name,
object_name=object_name,
)
except Exception as e:
logger.error(f"MinIO upload failed for dialog_processing: {e}")
raise HTTPException(status_code=500, detail="File upload to storage failed.")
# 로깅
endpoint_logger.log(model="N/A", input_filename=audio_file.filename)
# stt_fastapi에 Presigned URL 정보 전달
try:
async with httpx.AsyncClient() as client:
payload = {
"file_url": presigned_url,
"meeting_tag": meeting_tag,
}
resp = await client.post(
f"{MULTI_STT_API_BASE_URL}/dialog_processing", json=payload
)
return JSONResponse(status_code=resp.status_code, content=resp.json())
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"내부 서버 요청 실패: {e}")
@router.get("/start_parallel_stt/{meeting_tag}")
async def proxy_start_parallel_stt(meeting_tag: str):
async with httpx.AsyncClient() as client:
try:
resp = await client.get(
f"{MULTI_STT_API_BASE_URL}/start_parallel_stt/{meeting_tag}"
)
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"내부 서버 요청 실패: {e}")
return JSONResponse(status_code=resp.status_code, content=resp.json())
@router.get("/dialog_result/{task_id}")
async def proxy_get_progress(task_id: str):
async with httpx.AsyncClient() as client:
try:
resp = await client.get(
f"{MULTI_STT_API_BASE_URL}/result/parallel/{task_id}"
)
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"내부 서버 요청 실패: {e}")
return JSONResponse(status_code=resp.status_code, content=resp.json())

View File

@@ -0,0 +1,80 @@
# llmgateway/routers/stt_proxy.py
import io
import logging
import httpx
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
from utils.checking_keys import create_key
from utils.logging_utils import EndpointLogger
from utils.minio_utils import upload_file_to_minio_v2
router = APIRouter(tags=["YOLO Gateway"])
YOLO_BASE_URL = "http://yolo_gateway:8891" # docker-compose 내 서비스명 기반
logger = logging.getLogger(__name__)
@router.post("/detect_view")
async def proxy_audio(
request_info: Request,
image_file: UploadFile = File(...),
endpoint_logger: EndpointLogger = Depends(EndpointLogger),
):
request_id = create_key()
bucket_name = "yolo-gateway"
object_name = f"{request_id}/{image_file.filename}"
try:
presigned_url = upload_file_to_minio_v2(
file=image_file,
bucket_name=bucket_name,
object_name=object_name,
)
except Exception as e:
logger.error(f"MinIO upload failed: {e}")
raise HTTPException(status_code=500, detail="File upload to storage failed.")
endpoint_logger.log(
model="yolo11x", input_filename=image_file.filename, context_length=0
)
try:
async with httpx.AsyncClient() as client:
payload = {
"request_id": request_id,
"file_url": presigned_url,
}
response = await client.post(f"{YOLO_BASE_URL}/detect", json=payload)
return JSONResponse(content=response.json(), status_code=response.status_code)
except Exception as e:
raise HTTPException(status_code=500, detail=f"YOLO API 호출 실패: {str(e)}")
# YOLO 서버의 이미지 프록시
@router.get("/detect_view/images/{request_id}")
async def proxy_get_image(request_id: str):
try:
async with httpx.AsyncClient() as client:
yolo_url = f"{YOLO_BASE_URL}/images/{request_id}"
response = await client.get(yolo_url)
response.raise_for_status()
except httpx.HTTPError as e:
raise HTTPException(status_code=500, detail=f"YOLO 이미지 요청 실패: {str(e)}")
return StreamingResponse(io.BytesIO(response.content), media_type="image/jpeg")
# YOLO 서버의 JSON 결과 프록시
@router.get("/detect_view/results/{request_id}")
async def proxy_get_results(request_id: str):
try:
async with httpx.AsyncClient() as client:
yolo_url = f"{YOLO_BASE_URL}/results/{request_id}"
response = await client.get(yolo_url)
response.raise_for_status()
except httpx.HTTPError as e:
raise HTTPException(status_code=500, detail=f"YOLO 결과 요청 실패: {str(e)}")
return JSONResponse(content=response.json(), status_code=response.status_code)