다크 모드
Story Worker (Lambda)
개요
truloop-story는 SQS 기반 서버리스 Lambda 워커로, 룹의 미디어를 AI로 분석하여 리캡(코드: Story)을 자동 생성하는 서비스입니다. truloop-core가 SQS에 발행한 메시지를 소비하여, Gemini Vision으로 이미지를 분석하고 Claude로 블로그 스타일의 스토리를 생성한 뒤 결과를 truloop-core Internal API로 콜백합니다.
완전 Stateless 아키텍처로 DB를 사용하지 않으며, 모든 상태는 truloop-core가 관리합니다.
정보
truloop-ai-server와의 관계: truloop-ai-server는 하이라이트, 포스터, 이미지 분석 등 다양한 AI 콘텐츠를 Celery 기반으로 생성합니다. truloop-story는 리캡 생성만 전담하며, SQS + Lambda 기반의 독립적인 서버리스 아키텍처로 분리되어 있습니다. 리캡 생성의 긴 처리 시간과 독립적인 스케일링 요구사항을 반영한 설계입니다.
기술 스택
| 항목 | 기술 | 설명 |
|---|---|---|
| 런타임 | Python 3.13 | Lambda arm64 |
| IaC | Serverless Framework 4 | Lambda + SQS 인프라 관리 |
| AI 프레임워크 | pydantic-ai | Agent 기반 AI 모델 통합 |
| AI 프로바이더 | OpenRouter | Gemini Vision + Claude 라우팅 |
| 큐 | AWS SQS | 메시지 큐 + DLQ |
| HTTP | httpx | 비동기 HTTP 클라이언트 |
| 설정 | Pydantic Settings | 환경변수 기반 설정 관리 |
| 재시도 | tenacity | 지수 백오프 재시도 |
| 로깅 | loguru | CloudWatch 호환 구조화 로깅 |
주요 기능
| 기능 | 설명 |
|---|---|
| 이미지 분석 | Gemini Vision으로 각 이미지를 병렬 분석 (FallbackModel 지원) |
| 이미지 그룹핑 | 타임스탬프 기반 시간순 재배치 및 그룹핑 (30분 gap) |
| 스토리 생성 | Claude로 text/media 교차 패턴의 블로그 스타일 스토리 생성 |
| 다국어 지원 | 한국어, 일본어, 영어 3개 언어 완전 분리 프롬프트 |
| DLQ 처리 | 3회 실패 메시지를 DLQ에서 수신하여 Story를 FAILED로 마킹 |
프로젝트 구조
truloop-story/
├── handler.py # SQS 핸들러 (sqsWorker, dlqWorker)
├── config.py # Pydantic Settings 설정
├── models.py # Pydantic 모델 (StoryRequest, 블록 모델, API 모델)
├── services/
│ ├── image_analyzer.py # Gemini Vision 이미지 분석 (pydantic-ai Agent)
│ ├── image_grouper.py # 타임스탬프 기반 이미지 그룹핑 & 재배치
│ ├── story_generator.py # Claude 스토리 생성 (pydantic-ai Agent)
│ ├── locale.py # 로케일별 프롬프트/설정 (ko/ja/en)
│ └── api_client.py # truloop-core Internal API 클라이언트
├── serverless.yml # Serverless Framework 인프라 설정
└── pyproject.toml # 의존성처리 파이프라인
SQS 메시지 스키마
truloop-core가 발행하는 SQS 메시지의 구조입니다.
json
{
"request_id": "UUID (멱등성 보장용)",
"story_eid": "Story EID",
"locale": "ko | ja | en (nullable)",
"media_items": [
{
"media_url": "https://...",
"media_eid": "media EID",
"media_type": "image",
"width": 1080,
"height": 1920,
"timestamp": "2026-03-10T15:30:00+09:00 (timezone-aware, nullable)",
"comments": "JSON (nullable)",
"tagged_users": ["이름1", "이름2"]
}
],
"loop_metadata": {
"loop_eid": "loop EID",
"title": "모임 제목",
"description": "모임 설명",
"location": {
"display_name": "장소명",
"place_id": "...",
"map_provider": "GOOGLE_PLACES | KAKAO",
"loc": [127.0, 37.5]
},
"datetime": "2026-03-10T18:00:00Z",
"timezone": "Asia/Seoul",
"participants": ["참가자1", "참가자2"]
}
}API 콜백
처리 완료 후 truloop-core Internal API로 결과를 전송합니다. 인증은 Bearer API Key 방식입니다.
엔드포인트: POST /core/internal/stories/{story_eid}/requests/{request_id}
json
{
"status": "completed",
"title": "스토리 제목",
"thumbnail_url": "첫 번째 이미지 URL",
"preview_text": "2-3문장 미리보기",
"content_blocks": {
"blocks": [
{ "type": "text", "order": 0, "content": "텍스트 내용" },
{ "type": "media", "order": 1, "media_eid": "...", "media_url": "...", "media_type": "image", "width": 1080, "height": 1920 },
{ "type": "text", "order": 2, "content": "다음 텍스트" }
]
}
}환경변수
| 변수명 | 타입 | 소스 | 설명 |
|---|---|---|---|
OPENROUTER_API_KEY | string | SSM | OpenRouter API 키 |
OPENROUTER_BASE_URL | string | 기본값 | https://openrouter.ai/api/v1 |
IMAGE_ANALYSIS_MODELS | string | 기본값 | 파이프(|) 구분 fallback 모델 목록 |
STORY_GENERATION_MODEL | string | 기본값 | anthropic/claude-3.5-sonnet |
TRULOOP_CORE_API_URL | string | stage별 | dev: https://dev-api.truloop.app |
INTERNAL_API_KEY | string | SSM | Internal API 인증 키 |
STAGE | enum | Serverless | dev | prod |
LOG_LEVEL | string | 기본값 | INFO |
정보
SSM Parameter Store 경로: /truloop-story/{stage}/OPENROUTER_API_KEY, /truloop-story/{stage}/INTERNAL_API_KEY
인프라 리소스
Lambda Functions
| 함수 | 핸들러 | Timeout | Memory | 역할 |
|---|---|---|---|---|
sqsWorker | handler.sqs_worker_handler | 300s | 1024MB | 메인 스토리 생성 처리 |
dlqWorker | handler.dlq_handler | 30s | 256MB | 실패 메시지 처리 (FAILED 마킹) |
SQS Queues
| 큐 | 이름 패턴 | 설정 |
|---|---|---|
| Main Queue | {stage}-truloop-story-queue | VisibilityTimeout 360s, Long polling 20s, 14일 보관 |
| DLQ | {stage}-truloop-story-dlq | maxReceiveCount 3, 14일 보관 |
에러 처리 전략
| 상황 | 동작 | SQS 재시도 |
|---|---|---|
| 일부 이미지 분석 실패 | 성공한 이미지만으로 스토리 생성 계속 진행 | X |
| 전체 이미지 분석 실패 | API에 FAILED 보고 후 success=True 반환 | X (재시도 방지) |
| 스토리 생성 실패 | API에 FAILED 보고 후 success=True 반환 | X (재시도 방지) |
| API 보고 자체 실패 | success=False 반환 | O (SQS 재시도) |
| 3회 재시도 후 최종 실패 | DLQ로 이동 → dlqWorker가 FAILED 마킹 | X (DLQ 처리) |
주의
핵심 설계 원칙: 에러를 API에 보고할 수 있으면 success=True를 반환하여 SQS 재시도를 방지합니다. API 보고 자체가 실패한 경우에만 SQS 재시도가 발생합니다.
하위 문서
- 아키텍처 — 처리 흐름, 이미지 분석/스토리 생성 파이프라인, 다국어 지원, 에러 처리 상세
관련 문서
- 비즈니스 규칙은 리캡 생성 참조
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-11 | 최초 작성 — truloop-story 서비스 기술 문서 |