diff --git a/app/crud.py b/app/crud.py index a222f1c..70af968 100644 --- a/app/crud.py +++ b/app/crud.py @@ -116,4 +116,10 @@ def get_summaries_up_to(db: Session, movie_id: int, summary_id: int) -> List[Mov .filter(MovieManagerSummary.movie_id == movie_id)\ .filter(MovieManagerSummary.summary_id <= summary_id)\ .order_by(MovieManagerSummary.summary_id)\ - .all() \ No newline at end of file + .all() + +def get_custom_prompts(db: Session, movie_id: int) -> Optional[List[str]]: + """영화의 커스텀 프롬프트들 조회""" + summaries = db.query(Movie).filter(Movie.id == movie_id).first() + if summaries and summaries.custom_prompts: + return summaries.custom_prompts \ No newline at end of file diff --git a/app/models.py b/app/models.py index 51bf090..7b5577e 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,7 @@ -from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, BigInteger +from sqlalchemy import Column, String, Text, DateTime, ForeignKey, BigInteger from sqlalchemy.orm import relationship from sqlalchemy.sql import func +from sqlalchemy.dialects.postgresql import ARRAY from app.database import Base class Movie(Base): @@ -11,6 +12,8 @@ class Movie(Base): status = Column(String(50), default="PENDING") # PENDING, PROCEEDING[N/M], ORGANIZING, COMPLETE, FAILED_* created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + custom_prompts = Column(ARRAY(String), nullable=True) # 관계 설정 summaries = relationship("MovieManagerSummary", back_populates="movie") diff --git a/app/routers/marengo.py b/app/routers/marengo.py new file mode 100644 index 0000000..3b9c556 --- /dev/null +++ b/app/routers/marengo.py @@ -0,0 +1,25 @@ +# app/routers/marengo.py + +from fastapi import APIRouter, HTTPException +from app.services.marengo_service import init_marengo_client, get_marengo_response +from app.schemas import ChatRequest, ChatResponse + +router = APIRouter(prefix="/marengo", tags=["marengo"]) + +# init_marengo_client 하던 곳 +init_marengo_client() + +@router.post("", response_model=ChatResponse) +def chat_endpoint(req: ChatRequest): + """ + 사용자가 보낸 메시지를 Marengo API에 전달 후, 임베딩 벡터를 반환합니다. + """ + try: + result = get_marengo_response(req.message) + print("Raw response from Marengo:", result) + # If the result is a list of TextBlock, concatenate their text fields + if isinstance(result, list): + result = "".join(block.text for block in result) + return ChatResponse(response=result) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Marengo API 호출 오류: {e}") \ No newline at end of file diff --git a/app/schemas.py b/app/schemas.py index 11c0ba8..d84246f 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -90,6 +90,5 @@ class MovieManagerRequest(BaseModel): threshold: float = 30.0 class MovieManagerResponse(BaseModel): - final_story: str # 전체 줄거리 - final_review: str # 전체 평론 + prompt2results: List[tuple] thumbnail_folder_uri: str = None # 썸네일 후보 폴더 URI \ No newline at end of file diff --git a/app/services/marengo_service.py b/app/services/marengo_service.py new file mode 100644 index 0000000..cad7a7d --- /dev/null +++ b/app/services/marengo_service.py @@ -0,0 +1,53 @@ +# app/services/marengo_service.py + +from dotenv import load_dotenv +import boto3 +import os +import json + +load_dotenv() + +marengo_client = None +MARENGO_MODEL_ID = None + +def init_marengo_client(): + """ + 애플리케이션 시작 시 한 번만 호출되어야 하는 함수로, + 환경변수에서 자격증명과 모델 ID를 읽어 Bedrock 클라이언트를 초기화합니다. + """ + global marengo_client, MARENGO_MODEL_ID + + if marengo_client is not None: + return # 이미 초기화된 경우 재할당하지 않음 + + aws_key = os.getenv("AWS_ACCESS_KEY_ID") + aws_secret = os.getenv("AWS_SECRET_ACCESS_KEY") + aws_region = os.getenv("AWS_DEFAULT_REGION") or "us-east-1" + MARENGO_MODEL_ID = "apac." + os.getenv("MARENGO_MODEL_ID") + + if not (aws_key and aws_secret and MARENGO_MODEL_ID): + raise RuntimeError("필수 환경 변수가 설정되지 않았습니다: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, MARENGO_MODEL_ID") + + marengo_client = boto3.client(service_name='bedrock-runtime', + region_name=aws_region, + aws_access_key_id=aws_key, + aws_secret_access_key=aws_secret) + +def get_marengo_response(user_message: str) -> str: + """ + Bedrock Marengo API를 호출하여 텍스트 응답을 반환합니다. + """ + + message = { + "inputType": "text", + "inputText": user_message + } + + if marengo_client is None: + raise RuntimeError("Marengo Bedrock 클라이언트가 초기화되지 않았습니다.") + response = marengo_client.invoke_model( + modelId=MARENGO_MODEL_ID, + body=json.dumps(message) + ) + + return response.content \ No newline at end of file diff --git a/app/services/moviemanager_service.py b/app/services/moviemanager_service.py index 10ca75f..117c7f6 100644 --- a/app/services/moviemanager_service.py +++ b/app/services/moviemanager_service.py @@ -6,7 +6,7 @@ from app.services.transcribe_service import transcribe_video from app.services.scene_service import get_video_scenes from app.services.video_chunk_service import generate_video_chunks_info, extract_chunk_for_processing, cleanup_chunk_file -from app.crud import create_or_update_summary, get_summaries, get_summaries_up_to, delete_summaries_from, update_movie_status, mark_movie_failed, get_resume_info, get_movie +from app.crud import create_or_update_summary, get_summaries_up_to, delete_summaries_from, update_movie_status, mark_movie_failed, get_resume_info, get_movie, get_custom_prompts from app.database import SessionLocal import asyncio @@ -14,52 +14,39 @@ def load_prompts() -> Dict[str, str]: """ prompts.txt 파일에서 프롬프트 템플릿을 로드합니다. """ - try: - prompts_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "prompts.txt") - - with open(prompts_file_path, 'r', encoding='utf-8') as f: - content = f.read() - - prompts = {} - # 줄 단위로 파싱하여 섹션을 식별 - lines = content.split('\n') - current_section = None - current_content = [] - - for line in lines: - # 섹션 헤더 식별 (줄의 시작과 끝이 []로 둘러싸인 경우) - if line.strip().startswith('[') and line.strip().endswith(']') and not line.strip().startswith('[현재') and not line.strip().startswith('[등장'): - # 이전 섹션 저장 - if current_section and current_content: - prompts[current_section] = '\n'.join(current_content).strip() - - # 새 섹션 시작 - current_section = line.strip()[1:-1] # [ ] 제거 - current_content = [] - else: - # 섹션 내용 추가 - if current_section: - current_content.append(line) - - # 마지막 섹션 저장 - if current_section and current_content: - prompts[current_section] = '\n'.join(current_content).strip() - - print(f"📄 프롬프트 템플릿 로드 완료: {list(prompts.keys())}") - return prompts - - except FileNotFoundError: - print("⚠️ prompts.txt 파일을 찾을 수 없습니다. 기본 프롬프트를 사용합니다.") - return { - "VIDEO_ANALYSIS_PROMPT": "[등장인물 정보]\n{characters_info}\n\n다음은 연속된 비디오 시리즈의 일부입니다.{context}[현재 영상의 대화 내용]\n{conversation}\n\n[현재 영상의 장면별 시작 시각]\n{scene_times}\n\n등장인물 정보와 최근 영상들의 맥락을 고려하여 현재 영상에 대해:\n1. 각 장면이 보여주는 상황을 설명해주세요\n2. 대화 내용과 연관지어 설명해주세요\n3. 최근 영상들과의 연결점이나 스토리 진행을 분석해주세요\n\n현재 영상의 내용을 요약해주세요.", - "FINAL_SUMMARY_PROMPT": "[등장인물 정보]\n{characters_info}\n\n다음은 연속된 비디오 시리즈의 각 영상별 요약입니다:\n\n{all_summaries}\n\n등장인물 정보와 위 내용을 바탕으로:\n1. 전체 스토리의 흐름을 정리해주세요\n2. 주요 등장인물과 그들의 관계를 설명해주세요\n3. 핵심 사건들과 갈등 구조를 분석해주세요\n4. 전체 영상 시리즈의 주제와 메시지를 요약해주세요\n\n최종적으로 전체 영상 시리즈에 대한 종합적인 요약을 제공해주세요." - } - except Exception as e: - print(f"⚠️ 프롬프트 로드 중 오류: {str(e)}. 기본 프롬프트를 사용합니다.") - return { - "VIDEO_ANALYSIS_PROMPT": "[등장인물 정보]\n{characters_info}\n\n다음은 연속된 비디오 시리즈의 일부입니다.{context}[현재 영상의 대화 내용]\n{conversation}\n\n[현재 영상의 장면별 시작 시각]\n{scene_times}\n\n등장인물 정보와 최근 영상들의 맥락을 고려하여 현재 영상에 대해:\n1. 각 장면이 보여주는 상황을 설명해주세요\n2. 대화 내용과 연관지어 설명해주세요\n3. 최근 영상들과의 연결점이나 스토리 진행을 분석해주세요\n\n현재 영상의 내용을 요약해주세요.", - "FINAL_SUMMARY_PROMPT": "[등장인물 정보]\n{characters_info}\n\n다음은 연속된 비디오 시리즈의 각 영상별 요약입니다:\n\n{all_summaries}\n\n등장인물 정보와 위 내용을 바탕으로:\n1. 전체 스토리의 흐름을 정리해주세요\n2. 주요 등장인물과 그들의 관계를 설명해주세요\n3. 핵심 사건들과 갈등 구조를 분석해주세요\n4. 전체 영상 시리즈의 주제와 메시지를 요약해주세요\n\n최종적으로 전체 영상 시리즈에 대한 종합적인 요약을 제공해주세요." - } + prompts_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "prompts.txt") + + with open(prompts_file_path, 'r', encoding='utf-8') as f: + content = f.read() + + prompts = {} + # 줄 단위로 파싱하여 섹션을 식별 + lines = content.split('\n') + current_section = None + current_content = [] + + for line in lines: + # 섹션 헤더 식별 (줄의 시작과 끝이 []로 둘러싸인 경우) + if line.strip().startswith('[') and line.strip().endswith(']') and not line.strip().startswith('[현재') and not line.strip().startswith('[등장'): + # 이전 섹션 저장 + if current_section and current_content: + prompts[current_section] = '\n'.join(current_content).strip() + + # 새 섹션 시작 + current_section = line.strip()[1:-1] # [ ] 제거 + current_content = [] + else: + # 섹션 내용 추가 + if current_section: + current_content.append(line) + + # 마지막 섹션 저장 + if current_section and current_content: + prompts[current_section] = '\n'.join(current_content).strip() + + print(f"📄 프롬프트 템플릿 로드 완료: {list(prompts.keys())}") + return prompts + def natural_sort_key(s: str) -> List: """ @@ -333,7 +320,7 @@ def collect_thumbnail_info(video_summaries: List[Dict], s3_video_uri: str = None "urls": [] } -async def create_final_summary(video_summaries: List[str], characters_info: str) -> str: +async def create_final_results(video_summaries: List[str], custom_prompts: List[str], characters_info: str) -> List[tuple]: """ 모든 비디오 요약을 종합하여 최종 요약을 생성합니다. """ @@ -344,58 +331,67 @@ async def create_final_summary(video_summaries: List[str], characters_info: str) model_id = os.getenv("CLAUDE_MODEL_ID") # 프롬프트 템플릿 로드 - prompts = load_prompts() - template = prompts.get("FINAL_SUMMARY_PROMPT", "") + pre_prompts = load_prompts() + + # 각 입력 프롬프트 가져오기. + template = pre_prompts.get("FINAL_SUMMARY_PROMPT", "") # 모든 요약을 하나로 합침 all_summaries = "\n\n".join([ f"영상 {i+1}:\n{summary}" for i, summary in enumerate(video_summaries) ]) - - # 템플릿에 변수 삽입 - prompt = template.format( - characters_info=characters_info, - all_summaries=all_summaries - ) - # 디버깅: 최종 요약 프롬프트 출력 - print("=" * 80) - print("🎬 FINAL SUMMARY PROMPT INPUT:") - print("=" * 80) - print(prompt) - print("=" * 80) + final_responses = [] - request_body = { - "anthropic_version": "bedrock-2023-05-31", - "max_tokens": 4096, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": prompt - } - ] - } - ] - } - - response = bedrock.invoke_model( + # get all prompts and answers + for index, current_prompt in enumerate(custom_prompts): + prompt = current_prompt + "\nthe sentence bleow describes the video.\n" + all_summaries\ + + "\nthe sentence below shows the information of the character\n" + characters_info + + # 디버깅: 최종 요약 프롬프트 출력 + print("=" * 80) + print(f"🎬 FINAL SUMMARY PROMPT INPUT {index + 1}:") + print("=" * 80) + print(prompt) + print("=" * 80) + + request_body = { + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 4096, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": prompt + } + ] + } + ] + } + + response = bedrock.invoke_model( modelId=model_id, body=json.dumps(request_body) - ) - response_body = json.loads(response['body'].read()) - final_response = response_body['content'][0]['text'] - - # 디버깅: 최종 요약 답변 출력 - print("🎭 FINAL SUMMARY RESPONSE:") - print("=" * 80) - print(final_response) - print("=" * 80) - - return final_response + ) + + response_body = json.loads(response['body'].read()) + final_response = response_body['content'][0]['text'] + + # 디버깅: 최종 요약 답변 출력 + print(f"🎭 SUMMARY RESPONSE {index + 1}:") + print("=" * 80) + print(final_response) + print("=" * 80) + + result_tuple = (current_prompt, final_response) + + final_responses.append(result_tuple) + + return final_responses + async def process_single_video(s3_video_uri: str, characters_info: str, movie_id: int, segment_duration: int = 600, init: bool = False, @@ -600,10 +596,16 @@ async def process_single_video(s3_video_uri: str, characters_info: str, movie_id update_movie_status(db, movie_id, "ORGANIZING") db.close() print(f"📊 Movie 상태 업데이트: ORGANIZING") + + # 커스텀 프롬프트 가져오기 + db = SessionLocal() + custom_prompts = get_custom_prompts(db, movie_id) + db.close() + print(f"프롬프트 {len(custom_prompts)}개 로드 완료 for 최종 요약 생성") - print("🎭 최종 종합 요약 생성 중...") - # 최종 종합 요약 생성 - final_summary = await create_final_summary([vs["summary"] for vs in video_summaries], characters_info) + print("🎭 최종 프롬프트 응답 결과 생성 중...") + # 최종 프롬프트 응답 결과 생성 + final_summary = await create_final_results([vs["summary"] for vs in video_summaries], custom_prompts, characters_info) print(f"✅ 최종 요약 생성 완료 (길이: {len(final_summary)} 문자)") # 최종 요약도 데이터베이스에 저장 (모든 청크 다음 순서) @@ -626,15 +628,14 @@ async def process_single_video(s3_video_uri: str, characters_info: str, movie_id print("🎉 모든 청크 처리 완료!") print("=" * 80) - # 최종 요약을 줄거리와 평론으로 분리 - parsed_summary = parse_final_summary(final_summary) + # 최종 요약을 줄거리와 평론으로 분리 (이제 필요 없다.) + # parsed_summary = parse_final_summary(final_summary) # 썸네일 정보 수집 thumbnail_info = collect_thumbnail_info(video_summaries, s3_video_uri) return { - "final_story": parsed_summary["story"], - "final_review": parsed_summary["review"], + "prompt2results": final_summary, "thumbnail_folder_uri": thumbnail_info["folder_uri"] } @@ -894,10 +895,16 @@ async def process_videos_from_folder(s3_folder_path: str, characters_info: str, update_movie_status(db, movie_id, "ORGANIZING") db.close() print(f"📊 Movie 상태 업데이트: ORGANIZING") + + # 커스텀 프롬프트 가져오기 + db = SessionLocal() + custom_prompts = get_custom_prompts(db, movie_id) + db.close() + print(f"프롬프트 {len(custom_prompts)}개 로드 완료 for 최종 요약 생성") print("🎭 최종 종합 요약 생성 중...") - # 최종 종합 요약 생성 - final_summary = await create_final_summary([vs["summary"] for vs in video_summaries], characters_info) + # 최종 프롬프트 응답 결과 생성 + final_summary = await create_final_results([vs["summary"] for vs in video_summaries], custom_prompts, characters_info) print(f"✅ 최종 요약 생성 완료 (길이: {len(final_summary)} 문자)") # 최종 요약도 데이터베이스에 저장 (모든 비디오 다음 순서) @@ -920,7 +927,7 @@ async def process_videos_from_folder(s3_folder_path: str, characters_info: str, print("🎉 모든 비디오 처리 완료!") print("=" * 80) - # 최종 요약을 줄거리와 평론으로 분리 + # 최종 요약을 줄거리와 평론으로 분리 (이제 필요 없다.) parsed_summary = parse_final_summary(final_summary) # 썸네일 정보 수집 (폴더 모드에서는 폴더 URI 없음) diff --git a/examples/marengo_script.py b/examples/marengo_script.py new file mode 100644 index 0000000..6fb8bc5 --- /dev/null +++ b/examples/marengo_script.py @@ -0,0 +1,32 @@ +# Run model invocation with InvokeModel +import boto3 +import json +import os +from dotenv import load_dotenv + +# Create the model-specific input +model_id = "twelvelabs.marengo-embed-2-7-v1:0" +# Replace the us prefix depending on your region +inference_profile_id = "apac.twelvelabs.marengo-embed-2-7-v1:0" + +model_input = { + "inputType": "text", + "inputText": "man walking a dog" +} + +load_dotenv() + +# Initialize the Bedrock Runtime client +client = boto3.client(service_name='bedrock-runtime', + region_name=os.getenv("AWS_DEFAULT_REGION")) + +# Make the request +response = client.invoke_model( + modelId=inference_profile_id, + body=json.dumps(model_input) +) + +# Print the response body +response_body = json.loads(response['body'].read().decode('utf-8')) + +print(response_body) \ No newline at end of file diff --git a/main.py b/main.py index 7333ebe..2ada1da 100755 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI # from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv -from app.routers import chat, transcribe, scene, summarize, pipeline, moviemanager +from app.routers import chat, transcribe, scene, summarize, pipeline, moviemanager, marengo load_dotenv() @@ -18,6 +18,7 @@ # ) app.include_router(chat.router) +app.include_router(marengo.router) app.include_router(transcribe.router) app.include_router(scene.router) app.include_router(summarize.router)