1. 랭체인 이미지 인식 후 처리 마무리
2. fastAPI로 프로젝트 구조 실습 1. 랭체인 이미지 인식 후 처리 마무리 2. fastAPI로 프로젝트 구조 실습
This commit is contained in:
Generated
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CsvFileAttributes">
|
||||
<option name="attributeMap">
|
||||
<map>
|
||||
<entry key="/data/restaurant_reviews.csv">
|
||||
<value>
|
||||
<Attribute>
|
||||
<option name="separator" value="," />
|
||||
</Attribute>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="/data/reviews_mixed.csv">
|
||||
<value>
|
||||
<Attribute>
|
||||
<option name="separator" value="," />
|
||||
</Attribute>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PortForwardingSettings">
|
||||
<ports>
|
||||
<entry key="7860">
|
||||
<ForwardedPortInfo>
|
||||
<option name="hostPort" value="7860" />
|
||||
<option name="readOnly" value="false" />
|
||||
</ForwardedPortInfo>
|
||||
</entry>
|
||||
<entry key="7861">
|
||||
<ForwardedPortInfo>
|
||||
<option name="hostPort" value="7861" />
|
||||
<option name="readOnly" value="false" />
|
||||
</ForwardedPortInfo>
|
||||
</entry>
|
||||
</ports>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (ollama)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="~/Source/ollama/.venv" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PyToolsState">
|
||||
<option name="tools">
|
||||
<map>
|
||||
<entry key="black">
|
||||
<value>
|
||||
<ToolEntry />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="pyrefly">
|
||||
<value>
|
||||
<ToolEntry />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="pyright">
|
||||
<value>
|
||||
<ToolEntry />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="ruff">
|
||||
<value>
|
||||
<ToolEntry />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="ty">
|
||||
<value>
|
||||
<ToolEntry />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,158 @@
|
||||
from langchain_ibm import WatsonxEmbeddings
|
||||
from langchain_ibm import ChatWatsonx
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_chroma import Chroma
|
||||
from langchain_ollama import ChatOllama
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_core.runnables import RunnablePassthrough
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from PIL import Image, ImageEnhance, ImageFilter
|
||||
import shutil
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
#########
|
||||
# 모델
|
||||
#########
|
||||
load_dotenv()
|
||||
|
||||
apikey = os.getenv("WATSONX_API_KEY")
|
||||
project_id = os.getenv("WATSONX_PROJECT_ID")
|
||||
watsonx_ai_url = os.getenv("WATSONX_URL")
|
||||
|
||||
watson_llm = ChatWatsonx(
|
||||
model_id="ibm/granite-4-h-small",
|
||||
url=f"{watsonx_ai_url}",
|
||||
api_key=f"{apikey}",
|
||||
project_id=f"{project_id}",
|
||||
max_tokens=2000,
|
||||
)
|
||||
|
||||
watson_embedding = WatsonxEmbeddings(
|
||||
model_id="ibm/granite-embedding-278m-multilingual",
|
||||
url=f"{watsonx_ai_url}",
|
||||
api_key=f"{apikey}",
|
||||
project_id=f"{project_id}"
|
||||
)
|
||||
|
||||
vision_llm = ChatOllama(model="minimax-m3:cloud", temperature=0)
|
||||
|
||||
parser = StrOutputParser()
|
||||
|
||||
print("모델 초기화 완료")
|
||||
print(f"LLM : ibm/granite-4-h-small")
|
||||
print(f"Embedding : ibm/granite-embedding-278m-multilingual")
|
||||
print(f"Vision LLM: minimax-m3:cloud")
|
||||
|
||||
|
||||
## 이미지 전처리
|
||||
def process_image(image_path):
|
||||
img = Image.open(image_path)
|
||||
img = img.convert("L")
|
||||
|
||||
# 대비 향상
|
||||
img = ImageEnhance.Contrast(img).enhance(2.0)
|
||||
|
||||
# 선명도 향상
|
||||
img = img.filter(ImageFilter.SHARPEN)
|
||||
|
||||
# 이건 안되나?
|
||||
# img = ImageEnhance.Sharpness(img).enhance(1.5)
|
||||
|
||||
# 크기지정 : 처리 속도 최적화
|
||||
img.thumbnail((1024, 1024))
|
||||
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
|
||||
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
### vision llm 텍스트 추출
|
||||
def extract_text_from_image(image_path):
|
||||
img_b64 = process_image((image_path))
|
||||
message = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": """이 문서의 이미지에서 텍스트를 추출해주세요
|
||||
- 표, 항목, 번호 등 구조를 유지하세요
|
||||
- 읽을 수 없는 부분은 [불명확] 으로 유지하세요.
|
||||
- 이미지 설명 없이 텍스트만 출력하세요.
|
||||
"""},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {'url': f'data:image/jpeg;base64,{img_b64}'},
|
||||
},
|
||||
]
|
||||
)
|
||||
return vision_llm.invoke([message]).content
|
||||
|
||||
## vectorstore 저장
|
||||
def build_vectorstore(texts, metadatas):
|
||||
split_docs = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
|
||||
|
||||
# Document()
|
||||
docs = []
|
||||
for text, meta in zip(texts, metadatas):
|
||||
chunks = split_docs.split_text(text)
|
||||
for i, chunk in enumerate(chunks):
|
||||
docs.append(Document(page_content=chunk, metadata={**meta, "chunk_id":i}))
|
||||
|
||||
db_path = "./db/multimodal_db"
|
||||
if Path(db_path).exists():
|
||||
shutil.rmtree(db_path)
|
||||
|
||||
vectorstore = Chroma.from_documents(docs, watson_embedding, persist_directory=db_path)
|
||||
|
||||
return vectorstore
|
||||
|
||||
def format_docs(docs):
|
||||
return "\n\n".join(
|
||||
f'[출처 : {d.metadata.get("source", "?")}]\n{d.page_content}' for d in docs)
|
||||
|
||||
## rag 체인
|
||||
def build_rag_chain(vectorstore):
|
||||
retriever = vectorstore.as_retriever(search_kwargs = {"k": 8})
|
||||
|
||||
rag_prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
"다음 문서 내용을 참고하여 질문에 답하세요.\n"
|
||||
"문서에 없는 내용은 모른다고 답하세요.\n"
|
||||
"문서 내용 : \n{context}"
|
||||
),
|
||||
("human", "{question}")
|
||||
]
|
||||
)
|
||||
|
||||
parser = StrOutputParser()
|
||||
chain = {"context": retriever | format_docs, "question":RunnablePassthrough()} | rag_prompt | watson_llm | parser
|
||||
|
||||
return chain
|
||||
|
||||
def process_documnets(image_paths):
|
||||
texts = []
|
||||
metas = []
|
||||
for path in image_paths:
|
||||
text = extract_text_from_image(path)
|
||||
texts.append(text)
|
||||
metas.append({"source": path})
|
||||
|
||||
return build_vectorstore(texts = texts, metadatas = metas)
|
||||
|
||||
if __name__ == "__main__":
|
||||
images = ["./image/receipt1.jpg", "./image/receipt2.jpg", "./image/receipt3.jpg"]
|
||||
vectorstore = process_documnets(images)
|
||||
rag_chain = build_rag_chain(vectorstore)
|
||||
|
||||
question = ['총 금액은 얼마인가요?', "날짜가 언제인가요?"]
|
||||
|
||||
for q in question:
|
||||
print(f"Q: {q}")
|
||||
print(f"A: {rag_chain.invoke(q)}\n")
|
||||
+409
-132
@@ -19,12 +19,15 @@
|
||||
"metadata": {
|
||||
"collapsed": true,
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:40:13.650962123Z",
|
||||
"start_time": "2026-06-09T08:40:09.151285584Z"
|
||||
"end_time": "2026-06-15T03:06:07.002963942Z",
|
||||
"start_time": "2026-06-15T03:06:05.360626592Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"import base64\n",
|
||||
"\n",
|
||||
"from jedi.inference import analysis\n",
|
||||
"from langchain_core import documents\n",
|
||||
"from langchain_ibm import WatsonxEmbeddings\n",
|
||||
"from langchain_ibm import ChatWatsonx\n",
|
||||
"\n",
|
||||
@@ -41,23 +44,14 @@
|
||||
"from langchain_core.documents import Document\n",
|
||||
"from langchain_core.messages import HumanMessage"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/home/cooney/Source/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
||||
" from .autonotebook import tqdm as notebook_tqdm\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 1
|
||||
"outputs": [],
|
||||
"execution_count": 3
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:40:13.666828279Z",
|
||||
"start_time": "2026-06-09T08:40:13.654303979Z"
|
||||
"end_time": "2026-06-15T03:06:07.012203786Z",
|
||||
"start_time": "2026-06-15T03:06:07.003754555Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -71,13 +65,13 @@
|
||||
],
|
||||
"id": "27bd39a0da002140",
|
||||
"outputs": [],
|
||||
"execution_count": 2
|
||||
"execution_count": 4
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:40:17.037718935Z",
|
||||
"start_time": "2026-06-09T08:40:13.668291706Z"
|
||||
"end_time": "2026-06-15T03:06:09.243124960Z",
|
||||
"start_time": "2026-06-15T03:06:07.013041549Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -107,7 +101,7 @@
|
||||
],
|
||||
"id": "1b23141c1b60154a",
|
||||
"outputs": [],
|
||||
"execution_count": 3
|
||||
"execution_count": 5
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
@@ -118,8 +112,8 @@
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:40:17.084968096Z",
|
||||
"start_time": "2026-06-09T08:40:17.063355507Z"
|
||||
"end_time": "2026-06-15T03:06:09.262869259Z",
|
||||
"start_time": "2026-06-15T03:06:09.252393224Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -132,13 +126,13 @@
|
||||
],
|
||||
"id": "637e09cdf9c78eee",
|
||||
"outputs": [],
|
||||
"execution_count": 4
|
||||
"execution_count": 6
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:40:17.236651215Z",
|
||||
"start_time": "2026-06-09T08:40:17.091569162Z"
|
||||
"end_time": "2026-06-15T03:06:09.329919223Z",
|
||||
"start_time": "2026-06-15T03:06:09.264035014Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -168,13 +162,13 @@
|
||||
],
|
||||
"id": "a1114bd96f354025",
|
||||
"outputs": [],
|
||||
"execution_count": 5
|
||||
"execution_count": 7
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:41:42.833366570Z",
|
||||
"start_time": "2026-06-09T08:41:20.436085763Z"
|
||||
"end_time": "2026-06-15T03:06:21.590918755Z",
|
||||
"start_time": "2026-06-15T03:06:09.330837660Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -185,25 +179,30 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"content='이미지에는 **고양이**가 보입니다. 구체적인 특징을 살펴보면:\\n\\n- **갈색 줄무늬 고양이(태비)**로, 회색과 갈색이 섞인 줄무늬 무늬를 가지고 있습니다.\\n- **아름다운 청록색(옅은 파란색)의 눈동자**이 매우 인상적입니다.\\n- 고양이가 **입을 살짝 벌리고** 있어서 마치 울려고 하거나 야옹거릴 것 같은 표정을 짓고 있습니다.\\n- **긴 흰색 수염**이 양옆으로 길게 뻗어 있습니다.\\n- 고양이는 **나무 바닥** 위에 있는 것처럼 보이며, 따뜻한 **오렌지색 톤의 나무 결**이 배경에 드러나 있습니다.\\n- 위에서 내려다보는 각도로 촬영되어 고양이의 얼굴이 화면을 가득 채우고 있습니다.\\n\\n전체적으로 아기자기하면서도 고양이의 생기 넘치는 표정이 잘 포착된 사진입니다. 🐱' additional_kwargs={} response_metadata={'model': 'minimax-m3', 'created_at': '2026-06-09T08:41:42.722793354Z', 'done': True, 'done_reason': 'stop', 'total_duration': 21061167608, 'load_duration': None, 'prompt_eval_count': 511, 'prompt_eval_duration': None, 'eval_count': 237, 'eval_duration': None, 'logprobs': None, 'model_name': 'minimax-m3', 'model_provider': 'ollama'} id='lc_run--019eab8b-199f-7811-bea5-c25c73d68d14-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 511, 'output_tokens': 237, 'total_tokens': 748}\n"
|
||||
"content='이 이미지에서는 카메라를 올려다보는 고양이가 보입니다. 주요 특징은 다음과 같습니다:\\n\\n- **품종**: 얼룩무늬 고양이(태비)로, 회색, 검은색, 갈색이 섞인 줄무늬 패턴을 가지고 있습니다.\\n- **눈**: 크고 둥근 밝은 청록색/연한 녹색 눈동자로, 위쪽에서 비스듬히 내려다보는 각도로 촬영되었습니다.\\n- **표정**: 입이 살짝 벌어져 있어 마치 울거나 야옹하는 것처럼 보이며, 작은 이빨이 살짝 보입니다.\\n- **수염**: 길고 곧은 흰색 수염이 양쪽으로 뻗어 있습니다.\\n- **배경**: 따뜻한 톤의 나무 바닥이나 가구 위에 있는 것으로 보입니다.\\n\\n전체적으로 아기자기하면서도 약간 놀라 보이거나 무언가를 요구하는 듯한 표정의 고양이 클로즈업 사진입니다.' additional_kwargs={} response_metadata={'model': 'minimax-m3', 'created_at': '2026-06-15T03:06:21.490583355Z', 'done': True, 'done_reason': 'stop', 'total_duration': 11626185191, 'load_duration': None, 'prompt_eval_count': 511, 'prompt_eval_duration': None, 'eval_count': 231, 'eval_duration': None, 'logprobs': None, 'model_name': 'minimax-m3', 'model_provider': 'ollama'} id='lc_run--019ec93e-624f-7b11-89b0-cd5aabe517b3-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 511, 'output_tokens': 231, 'total_tokens': 742}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 8
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:06:21.612768204Z",
|
||||
"start_time": "2026-06-15T03:06:21.600081143Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": "#### 2. URL 이미지",
|
||||
"id": "79877a4f0166e70d",
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
"execution_count": 9
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:42:15.301506904Z",
|
||||
"start_time": "2026-06-09T08:41:45.482282423Z"
|
||||
"end_time": "2026-06-15T03:06:57.034661130Z",
|
||||
"start_time": "2026-06-15T03:06:21.613674680Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -233,34 +232,34 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'# 고양이 품종 분석\\n\\n이미지의 고양이는 **시베리안(Siberian)** 또는 **노르웨이 숲고양이(Norwegian Forest Cat)**로 보입니다. 특히 풍성한 이중 피모(double coat)와 체형으로 미루어 **시베리안 고양이**일 가능성이 높습니다.\\n\\n## 🐱 품종 특징\\n\\n### 외모\\n- **풍성한 장모**: 추운 서식지 환경에 적응한 이중 구조의 털\\n- **색상**: 화이트와 그레이의 클래릭 터비(고등어) 패턴\\n- **눈**: 크고 둥근 눈, 황금색~녹색 계열\\n- **체형**: 중대형의 다부진 몸매, 굵은 뼈대\\n\\n### 성격적 특징\\n- **온화하고 친근한 성격**: \"고양이계의 골든 리트리버\"라고 불릴 정도로 사람 친화적\\n- **높은 지능**: 문제 해결 능력이 뛰어나고 훈련이 잘 됨\\n- **사교적**: 다른 반려동물이나 아이들과도 잘 어울림\\n- **활발함**: 운동 능력이 뛰어나고 놀이를 좋아함\\n- **물에 대한 친화력**: 대부분의 고양이와 달리 물을 무서워하지 않음\\n\\n### 건강 관련\\n- **알레르기 저감형**: 다른 고양이 품종에 비해 **Fel d 1 단백질** 분비량이 적어 알레르기 반응이 적다고 알려져 있음\\n- **평균 수명**: 12~15년\\n- **유전 질환이 적어** 비교적 건강한 편\\n\\n### 역사\\n시베리안 고양이는 러시아 시베리아 지역이 원산지로, 수백 년 이상 야생에서 살아온 고양이가 길들여진 품종입니다. 자연 환경에 적응하며 진화한 대표적 토종 고양이 중 하나입니다.'"
|
||||
"'# 🐱 고양이 품종 분석\\n\\n이미지에 보이는 고양이는 **장모종(Long-haired breed)** 의 새끼 고양이로 보입니다.\\n\\n## 추정 품종\\n\\n### 1. 시베리안 고양이 (Siberian Cat)\\n가장 가능성이 높은 품종입니다.\\n- **러시아 원산**의 자연산 출생 품종\\n- 풍성한 이중 모피(Double coat)\\n- 추운 기후에 적응한 풍성한 꼬리(브러시 테일)\\n\\n### 2. 노르웨이 숲고양이 (Norwegian Forest Cat)\\n가능성이 있는 대체 품종\\n- **북유럽(노르웨이) 원산**\\n- 비슷한 외형의 대형 장모종\\n\\n### 3. 메인쿤 (Maine Coon)\\n가능성 있음\\n- 미국 원산의 대형 품종\\n- 귀 끝에 술(lynx tips)이 있는 것이 특징\\n\\n## 외형적 특징\\n\\n| 특징 | 설명 |\\n|------|------|\\n| **털 색깔** | 그레이 & 화이트 이색(Bicolor) |\\n| **모피** | 길고 부드러운 장모, 이중 코트 |\\n| **귀** | 크고 뾰족하며 끝에 작은 술이 있음 |\\n| **눈** | 크고 둥글며 밝은 색(황록색) |\\n| **체형** | 통통하고 근육질, 강건한 골격 |\\n| **꼬리** | 매우 풍성한 브러시 형태 |\\n\\n## 성격적 특징 (장모종 공통)\\n\\n- 🌟 **온화하고 친근한 성격**\\n- 🧠 **높은 지능과 호기심**\\n- 💧 **물을 좋아하는 편** (대부분의 고양이와 다름)\\n- 👨\\u200d👩\\u200d👧 **가족에게 애착이 강함**\\n- 🐾 **활동적이고 장난기 많음**\\n\\n## 관리 포인트\\n\\n- **정기적인 빗질**: 매일 1회, 털 엉킴 방지\\n- **철저한 위생 관리**: 장모는 배변 후 엉덩이 털 관리가 필요\\n- **운동 공간 확보**: 대형 품종으로 충분히 뛰어놀 수 있는 공간 필요\\n- **식단 관리**: 풍성한 모피 유지를 위한 고단백질 사료\\n\\n이처럼 사랑스럽고 아름다운 새끼 고양이는 성격이 매우 좋고, 가족과 함께하면 훌륭한 반려동물이 될 것입니다! 💕'"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 9
|
||||
"execution_count": 10
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:42:15.314335059Z",
|
||||
"start_time": "2026-06-09T08:42:15.303693807Z"
|
||||
"end_time": "2026-06-15T03:06:57.059273077Z",
|
||||
"start_time": "2026-06-15T03:06:57.045190160Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": "#### 3. OCR",
|
||||
"id": "9549854d2a4d9c89",
|
||||
"outputs": [],
|
||||
"execution_count": 10
|
||||
"execution_count": 11
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:42:15.335585069Z",
|
||||
"start_time": "2026-06-09T08:42:15.316787126Z"
|
||||
"end_time": "2026-06-15T03:06:57.080602805Z",
|
||||
"start_time": "2026-06-15T03:06:57.060282470Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -275,13 +274,13 @@
|
||||
],
|
||||
"id": "32f3f6d75760fcec",
|
||||
"outputs": [],
|
||||
"execution_count": 11
|
||||
"execution_count": 12
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:49:33.136027357Z",
|
||||
"start_time": "2026-06-09T08:49:09.992205257Z"
|
||||
"end_time": "2026-06-15T03:07:15.435586589Z",
|
||||
"start_time": "2026-06-15T03:06:57.083111604Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -297,7 +296,7 @@
|
||||
"\n",
|
||||
"message = HumanMessage(\n",
|
||||
" content = [\n",
|
||||
" {\"type\":\"text\", \"text\":\"이 문서의 텍스트를 추출해주세요\"},\n",
|
||||
" {\"type\":\"text\", \"text\":\"이 문서의 이미지에서 텍스트를 추출해주세요\"},\n",
|
||||
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
|
||||
"\n",
|
||||
" ]\n",
|
||||
@@ -315,50 +314,58 @@
|
||||
"# 문서 텍스트 추출\n",
|
||||
"\n",
|
||||
"## 미래로봇추진단(서울 근무)\n",
|
||||
"### S/W개발 - 시스템 소프트웨어\n",
|
||||
"### S/W개발 - 시스템/소프트웨어\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"### 포지션 소개 (Job Overview)\n",
|
||||
"**포지션 소개 (Job Overview)**\n",
|
||||
"휴머노이드 로봇의 실시간 제어 시스템 및 소프트웨어 플랫폼을 개발하는 직무입니다. 운영체제 환경 구성, 디바이스 드라이버, 실시간 제어 프레임워크 등 로봇 동작의 핵심 기반이 되는 소프트웨어를 설계 및 개발하며, 하드웨어 설계 조직 및 AI 연구 조직과 긴밀히 협업합니다.\n",
|
||||
"\n",
|
||||
"### 수행업무 (Job Details)\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"**수행업무 (Job Details)**\n",
|
||||
"- 휴머노이드 로봇의 실시간 제어 프레임워크를 설계하고 개발합니다.\n",
|
||||
"- 로봇, 센서 등 로봇 하드웨어 제어를 위한 디바이스 드라이버 및 하드웨어 추상화 계층을 개발합니다.\n",
|
||||
"- 로봇 제어를 운영체제(Linux 기반 실시간 OS 등) 환경을 구성하고 시스템 성능을 최적화합니다.\n",
|
||||
"- 로보, 센서 등 로봇 하드웨어 제어를 위한 디바이스 드라이버 및 하드웨어 추상화 계층을 개발합니다.\n",
|
||||
"- 로봇 제어를 운영체제(Linux 기반 실시간 OS 등) 환경에 구성하고 시스템 성능을 최적화합니다.\n",
|
||||
"- 유관 부서와 협업하여 로봇 조직 등 시 기능과 제어 시스템 간 연동 미들웨어를 개발합니다.\n",
|
||||
"\n",
|
||||
"### 자격요건 (Requirements)\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"**자격요건 (Requirements)**\n",
|
||||
"- 컴퓨터, 전기·전자, 기계, 로봇공학 등 관련 전공을 하신 분\n",
|
||||
"- 운영체제 기반 개념에 대한 이해도를 보유하신 분\n",
|
||||
"- 요구사항을 분석하여 소프트웨어를 구조적으로 설계 및 구현하는 역량을 보유하신 분\n",
|
||||
"- 다양한 분야의 엔지니어와 적극적으로 소통하며 협업할 수 있는 역량을 보유하신 분\n",
|
||||
"\n",
|
||||
"### 우대사항 (Preferences)\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"**우대사항 (Preferences)**\n",
|
||||
"- C/C++ 기반 시스템 프로그래밍 역량을 보유하신 분\n",
|
||||
"- Linux 기반 임베디드 시스템 개발 경험을 보유하신 분 (Kernel, Device Driver, BSP 등)\n",
|
||||
"- CAN, EtherCAT, UDP 등 하드웨어 통신 프로토콜 활용 경험을 보유하신 분\n",
|
||||
"- ROS2 기반 로봇제어 소프트웨어 수행 경험을 보유하신 분\n",
|
||||
"- Git 기반 협업 및 CI/CD 환경에서의 개발 경험을 보유하신 분\n",
|
||||
"\n",
|
||||
"### 커리어 비전 (Career Vision)\n",
|
||||
"휴머노이드 로봇의 초기 개발 단계부터 참여하여 핵심 개발자로 성장할 수 있습니다. 실시간 제어, 시스템 아키텍처, 로봇 미들웨어 등 다양한 경험을 통해 로봇제어 시스템 SW 전문가, 나아가 시스템 아키텍트로의 커리어 확장이 가능합니다.\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"**커리어 비전 (Career Vision)**\n",
|
||||
"휴머노이드 로봇의 초기 개발 단계부터 참여하여 핵심 개발자로 성장할 수 있습니다. 실시간 제어, 시스템 아키텍처, 로봇 미들웨어 등 다양한 경험을 통해 로봇틱스 시스템 SW 전문가, 나아가 시스템 아키텍트로의 커리어 확장이 가능합니다.\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"> **\"휴머노이드의 미래를 함께 실현할 분을 찾습니다\"**\n",
|
||||
"> **\"휴머노이드의 미래를 함께 설계할 분을 찾습니다\"**\n",
|
||||
"> \n",
|
||||
"> 로봇이 인공적으로 동작하기 위한 소프트웨어 기반을 함께 만들어 가며, 도전적인 기술 과제를 즐기는 분의 지원을 환영합니다.\n"
|
||||
"> 로봇이 인정적으로 동작하기 위한 소프트웨어 기반을 함께 만들어가자. 도전적인 기술 과제를 즐기는 분의 지원을 환영합니다.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 17
|
||||
"execution_count": 13
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:58:04.822594430Z",
|
||||
"start_time": "2026-06-09T08:58:04.802857234Z"
|
||||
"end_time": "2026-06-15T03:07:15.454781421Z",
|
||||
"start_time": "2026-06-15T03:07:15.444138572Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -381,13 +388,13 @@
|
||||
],
|
||||
"id": "d184430da51ccc36",
|
||||
"outputs": [],
|
||||
"execution_count": 20
|
||||
"execution_count": 14
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:58:41.448285681Z",
|
||||
"start_time": "2026-06-09T08:58:04.941406663Z"
|
||||
"end_time": "2026-06-15T03:07:32.850368373Z",
|
||||
"start_time": "2026-06-15T03:07:15.455557487Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -401,68 +408,67 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"# 두 제품의 비교 분석\n",
|
||||
"# 두 제품 비교 분석\n",
|
||||
"\n",
|
||||
"## 📦 제품 1: 냉장고 (이미지 1)\n",
|
||||
"## 🖼️ 이미지 1: 냉장고 (4도어 프렌치 도어형)\n",
|
||||
"\n",
|
||||
"| 항목 | 내용 |\n",
|
||||
"|------|------|\n",
|
||||
"| **제품 유형** | 4도어 프렌치 도어 냉장고 |\n",
|
||||
"| **외관 색상** | 화이트/크림 베이지 톤 |\n",
|
||||
"| **구조** | 상단 2도어 + 하단 2도어 (중앙 검은색 손잡이 바) |\n",
|
||||
"| **용도** | 식료품 보관 (냉동/냉장) |\n",
|
||||
"| **소재** | 금속 외관, 매트한 마감 |\n",
|
||||
"| **공간 배치** | 부엌에 독립적으로 설치 |\n",
|
||||
"### 주요 특징\n",
|
||||
"- **구조**: 4도어 프렌치 도어 디자인 (상단 2개 + 하단 2개)\n",
|
||||
"- **색상**: 화이트 / 크림색\n",
|
||||
"- **디자인**: 미니멀하고 깔끔한 라인\n",
|
||||
"- **디스플레이**: 중앙에 검은색 디지털 디스플레이 패널\n",
|
||||
"- **바퀴**: 하단 조절 가능한 발\n",
|
||||
"\n",
|
||||
"## 🍽️ 제품 2: 식탁/테이블 (이미지 2)\n",
|
||||
"\n",
|
||||
"| 항목 | 내용 |\n",
|
||||
"|------|------|\n",
|
||||
"| **제품 유형** | 4인용 다용도 식탁 (다이닝 테이블) |\n",
|
||||
"| **외관 색상** | 화이트 대리석 무늬 상판 + 블랙/골드 다리 |\n",
|
||||
"| **구조** | 직사각형 상판 + 4개의 슬림 다리 |\n",
|
||||
"| **용도** | 식사, 작업, 다용도 테이블 |\n",
|
||||
"| **소재** | 석재/대리석 패턴 (MDF 또는 천연석 추정) + 금속 다리 |\n",
|
||||
"| **공간 배치** | 거실/다이닝 공간에 배치 |\n",
|
||||
"### 용도\n",
|
||||
"주방용 대형 가전제품 (식품 보관)\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## 🔍 핵심 차이점 비교\n",
|
||||
"## 🖼️ 이미지 2: 식탁 / 다이닝 테이블\n",
|
||||
"\n",
|
||||
"### 1️⃣ **기능적 차이**\n",
|
||||
"- **냉장고**: 식품 보관·냉각이라는 **단일 기능성 가전**\n",
|
||||
"- **식탁**: 식사·작업·소통이 이루어지는 **생활 중심 가구**\n",
|
||||
"### 주요 특징\n",
|
||||
"- **소재**: 대리석 (화이트 + 그레이 veins) 상판\n",
|
||||
"- **다리**: 검은색/다크 컬러 + 골드(Gold) 팁\n",
|
||||
"- **형태**: 직사각형, 라운드 코너 처리\n",
|
||||
"- **구조**: 모던한 4각 다리형\n",
|
||||
"- **수납**: 하단 선반 구조\n",
|
||||
"\n",
|
||||
"### 2️⃣ **디자인 철학**\n",
|
||||
"- **냉장고**: 미니멀하고 깔끔한 모노톤, 현대 주방 인테리어에 맞춤\n",
|
||||
"- **식탁**: 모던 럭셔리 스타일, 대리석 + 골드 디테일로 **고급스러움 강조**\n",
|
||||
"\n",
|
||||
"### 3️⃣ **사용 맥락**\n",
|
||||
"- **냉장고**: 매일 사용되는 필수 가전 (실용성 중심)\n",
|
||||
"- **식탁**: 가족 모임, 게스트 접대 등 **라이프스타일 연출**\n",
|
||||
"\n",
|
||||
"### 4️⃣ **가격대**\n",
|
||||
"- **냉장고**: 일반적으로 4도어 모델은 **200~500만 원대** 이상\n",
|
||||
"- **식탁**: 소재와 브랜드에 따라 **30~200만 원대**로 다양\n",
|
||||
"\n",
|
||||
"### 5️⃣ **공간 점유**\n",
|
||||
"- **냉장고**: 벽면 붙박이로 수직 공간 활용\n",
|
||||
"- **식탁**: 공간 중앙에 위치하며 **수평적 공간 활용**\n",
|
||||
"### 용도\n",
|
||||
"거실/식당/다이닝 공간 가구\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## 💡 공통점\n",
|
||||
"두 제품 모두 **미니멀한 화이트 톤**으로 통일되어 있어 모던한 인테리어에 적합하며, 깔끔하고 세련된 실내 공간을 연출하는 데 기여하는 **생활 필수 아이템**입니다.\n"
|
||||
"## 📊 핵심 차이점 비교\n",
|
||||
"\n",
|
||||
"| 구분 | 냉장고 | 식탁 |\n",
|
||||
"|------|--------|------|\n",
|
||||
"| **카테고리** | 가전제품 | 가구 |\n",
|
||||
"| **소재** | 금속/플라스틱 | 대리석 + 금속 |\n",
|
||||
"| **용도** | 식품 보관·냉각 | 식사·작업 공간 |\n",
|
||||
"| **크기** | 대형 (약 180cm+) | 중형 (140~180cm) |\n",
|
||||
"| **가격대** | 고가 (100만원~) | 중~고가 |\n",
|
||||
"| **색감** | 모노톤 화이트 | 화이트+블랙+골드 |\n",
|
||||
"| **디자인 스타일** | 미니멀 모던 | 럭셔리 모던 |\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## 🎨 공통점\n",
|
||||
"- **모던한 디자인**: 두 제품 모두 현대적인 인테리어에 어울리는 깔끔한 스타일\n",
|
||||
"- **화이트 톤**: 밝고 깨끗한 인상을 주는 화이트 계열\n",
|
||||
"- **고급스러움**: 프리미엄 제품군의 디자인 철학\n",
|
||||
"\n",
|
||||
"## 💡 인테리어 활용\n",
|
||||
"두 제품 모두 **모던 미니멀** 또는 **럭셔리 모던** 스타일 주방에 잘 어울리며, 화이트 톤의 통일성으로 공간의 시너지를 극대화할 수 있습니다.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 21
|
||||
"execution_count": 15
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-09T08:59:26.775983373Z",
|
||||
"start_time": "2026-06-09T08:58:56.843217983Z"
|
||||
"end_time": "2026-06-15T03:08:00.503202012Z",
|
||||
"start_time": "2026-06-15T03:07:32.869952272Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@@ -480,57 +486,328 @@
|
||||
"\n",
|
||||
"## 📊 차트 1: 최근 6개월 전체 화장품 수출 추이\n",
|
||||
"\n",
|
||||
"| 기간 | 수출액(억 달러) | YoY 증감률 |\n",
|
||||
"|------|----------------|-----------|\n",
|
||||
"| 기간 | 수출액(백만$) | YoY 증감률 |\n",
|
||||
"|------|---------------|------------|\n",
|
||||
"| 25년 12월 | 883.7 | +10.2% |\n",
|
||||
"| 26년 1월 | 841.6 | **+33.7%** (최고점) |\n",
|
||||
"| 26년 2월 | 752.1 | +1.7% (최저점) |\n",
|
||||
"| 26년 1월 | 841.6 | **+33.7%** (최고) |\n",
|
||||
"| 26년 2월 | 752.1 | +1.7% (최저) |\n",
|
||||
"| 26년 3월 | 960.4 | +23.0% |\n",
|
||||
"| 26년 4월 | **1,096.3** (최고 수출액) | +21.7% |\n",
|
||||
"| 26년 5월(1~20일) | 671.1 | **-16.0%** (마이너스 전환) |\n",
|
||||
"| 26년 5월(1~20일) | 671.1 | **-16.0%** (전월 대비 급락) |\n",
|
||||
"\n",
|
||||
"## 📊 차트 2: 5월 1~20일 주요 국가별 일평균 수출액\n",
|
||||
"\n",
|
||||
"| 국가 | 일평균 수출액(백만 달러) | YoY 증감률 |\n",
|
||||
"|------|-------------------------|-----------|\n",
|
||||
"| 중화권(중국) | 13.13 | **+76.4%** |\n",
|
||||
"| 유럽 | 11.14 | +61.3% |\n",
|
||||
"| 국가 | 일평균 수출액(백만$) | YoY 증감률 |\n",
|
||||
"|------|---------------------|------------|\n",
|
||||
"| 중화권(중국) | 13.13 | **+76.4%** (최고) |\n",
|
||||
"| 미국 | 11.66 | +40.3% |\n",
|
||||
"| 유럽 | 11.14 | +61.3% |\n",
|
||||
"| 동남아 | 4.91 | +22.0% |\n",
|
||||
"| 일본 | 약 3.10 | **-14.1%** |\n",
|
||||
"| 일본 | 3.xx | **-14.1%** (유일한 마이너스) |\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"## 🔍 주요 변화 추이 분석\n",
|
||||
"\n",
|
||||
"### 1. **4월까지 성장 → 5월 급격한 반전**\n",
|
||||
"- 1~4월 동안 꾸준한 성장세(YoY +21~34%)를 유지하며 4월에 **사상 최고치 1,096.3억 달러**를 기록\n",
|
||||
"- 그러나 5월(1~20일)에 들어 **YoY -16.0%로 마이너스 전환**되며 성장세 급격히 둔화\n",
|
||||
"### 1️⃣ **전체 수출의 5월 급격한 반전**\n",
|
||||
"- 1~4월은 두 자릿수 성장(YoY +20~33%)을 유지하며 **우상향 흐름**이었으나, 5월(1~20일) 들어 **-16% 마이너스 성장**으로 전환\n",
|
||||
"- 4월 대비 약 425백만$ 감소하며 **6개월 중 최저 수출액** 기록\n",
|
||||
"\n",
|
||||
"### 2. **국가별 양극화 현상**\n",
|
||||
"- **📈 신흥 시장 강세**: 중화권(+76.4%), 유럽(+61.3%), 미국(+40.3%) 등 주요 시장에서 고른 성장\n",
|
||||
"- **📉 일본 시장 침체**: 유일하게 마이너스(-14.1%)를 기록하며 전체 수출액 둔화의 주요 원인으로 작용\n",
|
||||
"### 2️⃣ **국가별 양극화 현상**\n",
|
||||
"- ✅ **호조**: 중화권(76.4%), 유럽(61.3%), 미국(40.3%) 등 신흥·대형 시장은 여전히 고성장 지속\n",
|
||||
"- ❌ **악화**: 일본은 유일한 마이너스(-14.1%) 국가로, **일·중 갈등 리스크**(수출 규제 등)가 직접적 영향으로 작용한 것으로 추정\n",
|
||||
"\n",
|
||||
"### 3. **성장 동력의 재편**\n",
|
||||
"- 종전에는 일본이 주요 화장품 수출 대상국이었으나, 일본 시장 위축이 전체 지표에 영향\n",
|
||||
"- **중화권·유럽·미국 중심의 수출 구조 재편**이 가속화되는 흐름\n",
|
||||
"### 3️⃣ **시장 다변화 효과**\n",
|
||||
"- 5월 전체 수출 급감에도 **중국·유럽·미국** 비중이 절대적(1일 평균 11백만$ 이상)이라, 일본 위축이 전체 실적을 끌어내린 구조\n",
|
||||
"- 중화권 일평균 수출액(13.13M$)이 전체 5월 실적의 약 **39%**(671.1M$ ÷ 20일 = 33.6M$ 기준)를 차지\n",
|
||||
"\n",
|
||||
"### 4. **시사점**\n",
|
||||
"- 5월 마이너스 전환은 **일본 시장 감소**가 가장 큰 영향 요인\n",
|
||||
"- 단, 5월은 1~20일(부분 집계) 기간이므로 **후반부 회복 여부** 추가 모니터링 필요\n",
|
||||
"- 한·중·일 관계 변화, 환율, 관세 등 외부 변수 영향 지속 점검 요구됨\n"
|
||||
"### 4️⃣ **핵심 인사이트**\n",
|
||||
"> 📌 **\"성장 동력은 견고하나, 지정학적 리스크 노출도 확대\"**\n",
|
||||
"> - 美·유럽·중국의 K-뷰티 수요는 여전히 강력\n",
|
||||
"> - 그러나 **일본 1개국의 감소**가 전체 수치를 마이너스로 전환시킬 정도로 **일본 시장 의존도 리스크**가 부각\n",
|
||||
"> - 향후 **대일 수출 정상화 여부**가 6월 이후 실적 반등의 핵심 변수가 될 전망\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"**요약**: 두 차트를 종합하면, 2026년 상반기 화장품 수출은 **1~4월 고성장 → 5월 급락**의 V자 변동을 보였으며, 이는 **신흥 시장 확대(긍정)**와 **일본 시장 위축(부정)**의 상반된 요인이 결합된 결과로 해석됩니다.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 24
|
||||
"execution_count": 16
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### 멀티모달\n",
|
||||
" - 이미지 기반 문서 분석 시스템"
|
||||
],
|
||||
"id": "9687a7578057efa4"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:32.239301275Z",
|
||||
"start_time": "2026-06-15T03:32:32.212206906Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# 이미지 => 설명문 생성\n",
|
||||
"\n",
|
||||
"def image_to_caption(image_path):\n",
|
||||
" img_b64 = encode_image(image_path)\n",
|
||||
" message = HumanMessage(\n",
|
||||
" content = [\n",
|
||||
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
|
||||
" {\"type\":\"text\", \"text\": \"\"\"\n",
|
||||
" 이 이미지를 검색용 설명문으로 요약하세요.\n",
|
||||
" 200자 이내로 작성하세요.\n",
|
||||
" 핵심 객체와 텍스트만 포함하세요.\n",
|
||||
"\"\"\"}\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
" return vision_llm.invoke([message]).content"
|
||||
],
|
||||
"id": "972db1f4ffac7c6f",
|
||||
"outputs": [],
|
||||
"execution_count": 32
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:43.666106534Z",
|
||||
"start_time": "2026-06-15T03:32:32.758731810Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"caption = image_to_caption('./image/chart1.png')\n",
|
||||
"caption"
|
||||
],
|
||||
"id": "5b60e8a76d1a4d47",
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'최근 6개월 화장품 수출액 및 전년동기 대비(YoY) 증감률 추이를 보여주는 막대·선 혼합 차트. 25년 12월부터 26년 5월(1~20일)까지 수출액은 752.1~1096.3백만 달러 범위, YoY는 33.7%에서 -16%까지 변동. 4월 수출액 1096.3백만 달러로 최고, 5월 -16%로 최저 성장률 기록.'"
|
||||
]
|
||||
},
|
||||
"execution_count": 33,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 33
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:43.681737845Z",
|
||||
"start_time": "2026-06-15T03:32:43.668688923Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# Document\n",
|
||||
"\n",
|
||||
"def build_multimodal_index(image_dir:str, text_docs:list[Document]):\n",
|
||||
" all_docs = list(text_docs)\n",
|
||||
"\n",
|
||||
" # image_dir 안 파일 가져오기\n",
|
||||
" image_files = list(Path(image_dir).glob(\"*.{jpg, jpeg, png}\"))\n",
|
||||
"\n",
|
||||
" # 캡션 생성 => Document => 임베딩\n",
|
||||
" for img_path in image_files:\n",
|
||||
" caption = image_to_caption(image_path = img_path)\n",
|
||||
"\n",
|
||||
" doc = Document(page_content=caption, metadata={\n",
|
||||
" \"source\" : str(img_path),\n",
|
||||
" \"type\" : \"image\",\n",
|
||||
" \"image_path\" : str(img_path)\n",
|
||||
" })\n",
|
||||
" all_docs.append(doc)\n",
|
||||
"\n",
|
||||
" Chroma.from_documents(all_docs, watson_embedding, persist_directory=\"./db/multimodal_db\")"
|
||||
],
|
||||
"id": "96b45e3ca155e9ab",
|
||||
"outputs": [],
|
||||
"execution_count": 34
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:43.691778483Z",
|
||||
"start_time": "2026-06-15T03:32:43.682586147Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# 검색결과에 이미지 포함 여부 확인\n",
|
||||
"\n",
|
||||
"def search_with_images(vectorstore, query):\n",
|
||||
" results = vectorstore.similarity_search_with_relevance_scores(query, k=5)\n",
|
||||
" text_results = [doc for doc, score in results if doc.metadata.get(\"type\") != 'image']\n",
|
||||
" image_results = [doc for doc, score in results if doc.metadata.get(\"type\") == 'image']\n",
|
||||
" print(f\"텍스트 결과 : {len(text_results)}, 이미지 결과 {len(image_results)}\")\n",
|
||||
" return text_results, image_results"
|
||||
],
|
||||
"id": "8d26ef630c80b820",
|
||||
"outputs": [],
|
||||
"execution_count": 35
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:44.606069502Z",
|
||||
"start_time": "2026-06-15T03:32:43.694865760Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"image_doc = Document(\n",
|
||||
" page_content=caption,\n",
|
||||
" metadata = {\"type\" : \"image\", \"image_path\":\"./image/chart1.png\"}\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"docs = [\n",
|
||||
" Document(\n",
|
||||
" page_content=\"2025년 매출은 증가했다.\"\n",
|
||||
" ),\n",
|
||||
" image_doc\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"build_multimodal_index(\"./image\", docs)"
|
||||
],
|
||||
"id": "a8beb26afaf51f76",
|
||||
"outputs": [],
|
||||
"execution_count": 36
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:46.252600621Z",
|
||||
"start_time": "2026-06-15T03:32:44.616669574Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"vectorstore = Chroma(embedding_function=watson_embedding, persist_directory=\"./db/multimodal_db\")\n",
|
||||
"\n",
|
||||
"results = vectorstore.similarity_search(\"매출 추세\", k=3)\n",
|
||||
"\n",
|
||||
"for r in results:\n",
|
||||
" print(r.page_content)"
|
||||
],
|
||||
"id": "16976c61e211677c",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2025년 매출은 증가했다.\n",
|
||||
"2024년 분기별 매출 현황 막대그래프. Q1 120백만원(파랑), Q2 185백만원(초록), Q3 160백만원(주황), Q4 230백만원(빨강)으로 Q4가 최고치를 기록. 상단 좌측에 \"Q4 매출액 최고치\" 데이터 분석 메모 포함. 단위는 백만원.\n",
|
||||
"최근 6개월 화장품 수출액 및 전년동기 대비(YoY) 증감률 추이를 보여주는 막대·선 혼합 차트. 25년 12월부터 26년 5월(1~20일)까지 수출액은 752.1~1096.3백만 달러 범위, YoY는 33.7%에서 -16%까지 변동. 4월 수출액 1096.3백만 달러로 최고, 5월 -16%로 최저 성장률 기록.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 37
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:32:46.278685884Z",
|
||||
"start_time": "2026-06-15T03:32:46.264136623Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def multimodal_answer(vectorstore, question):\n",
|
||||
" text_results, image_results = search_with_images(vectorstore = vectorstore, query = question)\n",
|
||||
"\n",
|
||||
" # 텍스트 결과 하나의 컨텍스트로 생성\n",
|
||||
" text_context = \"\\n\\n\".join([r.page_content for r in text_results])\n",
|
||||
"\n",
|
||||
" # 이미지로 검색된 경우\n",
|
||||
" image_context = \"\"\n",
|
||||
" referenced_images = []\n",
|
||||
" for img_doc in image_results[:3]:\n",
|
||||
" img_path = img_doc.metadata.get(\"image_path\")\n",
|
||||
"\n",
|
||||
" img_b64 = encode_image(img_path)\n",
|
||||
" analysis = vision_llm.invoke([\n",
|
||||
" HumanMessage(\n",
|
||||
" content=[\n",
|
||||
" {\"type\": \"image_url\", \"image_url\": {'url': f'data:image/jpeg;base64,{img_b64}'}},\n",
|
||||
" {\"type\": \"text\", \"text\": f\"이 이미지에서 다음 질문과 관련된 내용을 설명하세요: {question}\"}\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
" ]).content\n",
|
||||
"\n",
|
||||
" image_context += f\"[이미지 분석: {img_path}]\\n{analysis}\\n\\n\"\n",
|
||||
" referenced_images.append(img_path)\n",
|
||||
"\n",
|
||||
" # 최종 답변\n",
|
||||
" combined_context = text_context + \"\\n\\n\" + image_context\n",
|
||||
"\n",
|
||||
" final_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" (\"system\", \"다음은 텍스트와 이미지 분석 결과를 참고하여 질문에 대한 답변입니다.\\n\\n{context}\"),\n",
|
||||
" (\"human\", \"{question}\")\n",
|
||||
" ])\n",
|
||||
"\n",
|
||||
" parser = StrOutputParser()\n",
|
||||
" chain = final_prompt | watson_llm | parser\n",
|
||||
"\n",
|
||||
" answer = chain.invoke({\n",
|
||||
" \"context\": combined_context,\n",
|
||||
" \"question\": question\n",
|
||||
" })\n",
|
||||
"\n",
|
||||
" return {\"answer\" : answer, \"images\": referenced_images}"
|
||||
],
|
||||
"id": "4a82feae327cb3a1",
|
||||
"outputs": [],
|
||||
"execution_count": 38
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2026-06-15T03:33:23.098722925Z",
|
||||
"start_time": "2026-06-15T03:32:46.280058605Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": "multimodal_answer(vectorstore, \"최근 6개월 전체 화장품 수출액?\")",
|
||||
"id": "e0dbe6998b4f75fd",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"텍스트 결과 : 2, 이미지 결과 2\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'answer': '최근 6개월 전체 화장품 수출액은 약 52억 달러입니다. 이는 6개월 동안의 월별 수출액을 합산한 결과입니다.',\n",
|
||||
" 'images': ['./image/chart1.png', 'sample_images/sales_chart_2024.jpg']}"
|
||||
]
|
||||
},
|
||||
"execution_count": 39,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 39
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": "",
|
||||
"id": "972db1f4ffac7c6f"
|
||||
"id": "bce01c1bf17b40e1",
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import gradio as gr
|
||||
from multimodal import multimodal_answer, search_with_images, build_multimodal_index
|
||||
from image_analyzer import process_documnets, build_rag_chain
|
||||
|
||||
vectorstore = None
|
||||
rag_chain = None
|
||||
|
||||
def upload_and_process(files):
|
||||
global vectorstore, rag_chain
|
||||
|
||||
if not files:
|
||||
return "파일을 업로드해주세요."
|
||||
|
||||
paths = [f.name for f in files]
|
||||
vectorstore = process_documnets(files)
|
||||
rag_chain = build_rag_chain(vectorstore)
|
||||
|
||||
return f"{len(paths)}개 문서 처리 완료!! 질문하세요"
|
||||
|
||||
def answer_question(question):
|
||||
if rag_chain is None:
|
||||
return "먼저 문서를 업로드 해 주세요."
|
||||
|
||||
return rag_chain.invoke(question)
|
||||
|
||||
with gr.Blocks(title="이미지 기반 문서 분석 시스템") as app:
|
||||
gr.Markdown("# 이미지 기반 문서 분석 시스템")
|
||||
gr.Markdown("스캔 문서, 영수증, 계약서 이미지를 업로드하면 질문할 수 있습니다.")
|
||||
|
||||
with gr.Row():
|
||||
file_input = gr.File(label="문서 이미지 업로드", file_types=[".jpg", ".jpeg", ".png", ".webp"], file_count="multiple",)
|
||||
status_bar = gr.Textbox(label="처리 상태", lines=3)
|
||||
|
||||
upload_btn = gr.Button("문서 분석 시작", variant="primary")
|
||||
upload_btn.click(fn=upload_and_process, inputs=[file_input], outputs=[status_bar])
|
||||
|
||||
gr.Markdown("---")
|
||||
question = gr.Textbox(label="질문 입력", placeholder="문서에서 찾고 싶은 내용을 입력하세요.")
|
||||
answer_box = gr.Textbox(label="답변", lines=8)
|
||||
ask_btn = gr.Button("질문하기")
|
||||
ask_btn.click(fn=answer_question, inputs=[question], outputs=[answer_box])
|
||||
|
||||
app.launch()
|
||||
@@ -0,0 +1,281 @@
|
||||
from langchain_ibm import WatsonxEmbeddings
|
||||
from langchain_ibm import ChatWatsonx
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_chroma import Chroma
|
||||
from langchain_ollama import ChatOllama
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import shutil
|
||||
|
||||
#########
|
||||
# 모델
|
||||
#########
|
||||
load_dotenv()
|
||||
|
||||
apikey = os.getenv("WATSONX_API_KEY")
|
||||
project_id = os.getenv("WATSONX_PROJECT_ID")
|
||||
watsonx_ai_url = os.getenv("WATSONX_URL")
|
||||
|
||||
watson_llm = ChatWatsonx(
|
||||
model_id="ibm/granite-4-h-small",
|
||||
url=f"{watsonx_ai_url}",
|
||||
api_key=f"{apikey}",
|
||||
project_id=f"{project_id}",
|
||||
max_tokens=2000,
|
||||
)
|
||||
|
||||
watson_embedding = WatsonxEmbeddings(
|
||||
model_id="ibm/granite-embedding-278m-multilingual",
|
||||
url=f"{watsonx_ai_url}",
|
||||
api_key=f"{apikey}",
|
||||
project_id=f"{project_id}"
|
||||
)
|
||||
|
||||
vision_llm = ChatOllama(model="minimax-m3:cloud", temperature=0)
|
||||
|
||||
parser = StrOutputParser()
|
||||
|
||||
print("모델 초기화 완료")
|
||||
print(f"LLM : ibm/granite-4-h-small")
|
||||
print(f"Embedding : ibm/granite-embedding-278m-multilingual")
|
||||
print(f"Vision LLM: minimax-m3:cloud")
|
||||
|
||||
|
||||
#########
|
||||
# 이미지 유틸리티 함수
|
||||
#########
|
||||
|
||||
def encode_image(image_path:str)->str:
|
||||
"""
|
||||
이미지 파일을 Base64 문자열로 인코딩
|
||||
"""
|
||||
path = Path(image_path)
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"파일이 존재하지 않습니다. : {image_path}")
|
||||
|
||||
with open(image_path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
#########
|
||||
# 샘플 테스트 데이터
|
||||
#########
|
||||
|
||||
def create_sample_images(image_dir = "./sample_images"):
|
||||
os.makedirs(image_dir, exist_ok =True)
|
||||
|
||||
# 기본 이미지 생성
|
||||
img1 = Image.new("RGB", (600, 400), color = (255, 255, 255))
|
||||
draw = ImageDraw.Draw(img1)
|
||||
|
||||
draw.text((180, 20), "2024 분기별 매출 현황 (억원)", fill=(30, 30, 30))
|
||||
|
||||
bars = [
|
||||
("Q1", 120, (70, 130, 180)),
|
||||
("Q2", 185, (60, 179, 113)),
|
||||
("Q3", 160, (255, 165, 0)),
|
||||
("Q4", 230, (255, 50, 50)),
|
||||
]
|
||||
bar_w, base_y = 80, 330
|
||||
|
||||
for i, (label, val, color) in enumerate(bars):
|
||||
x = 80 + i * 130
|
||||
h = val
|
||||
draw.rectangle([x, base_y - h, x + bar_w, base_y], fill=color)
|
||||
draw.text((x + 20, base_y - h - 20), f"{val}억", fill=(30, 30, 30))
|
||||
draw.text((x+25, base_y + 5), label, fill=(30, 30, 30))
|
||||
draw.line([(60, 50), (60, base_y)], fill=(0, 0, 0), width=2)
|
||||
draw.line([(60, base_y), (500, base_y)], fill=(0, 0, 0), width=2)
|
||||
draw.text((10, 15), "상승 추세 : Q4 최고치 달성", fill=(0,0,0))
|
||||
|
||||
img1.save(f"{image_dir}/sales_chart_2024.jpg")
|
||||
print(f"생성 : {image_dir}/sales_chart_2024.jpg")
|
||||
|
||||
return image_dir
|
||||
|
||||
def create_sample_text_docs():
|
||||
"""테스트용 샘플 텍스트 문서 생성"""
|
||||
docs = [
|
||||
Document(
|
||||
page_content="""
|
||||
2024년 연간 매출 보고서 요약
|
||||
|
||||
당사의 2024년 전체 매출은 695억원으로, 전년 대비 23% 성장을 달성했습니다.
|
||||
1분기(Q1) 120억원, 2분기(Q2) 185억원, 3분기(Q3) 160억원, 4분기(Q4) 230억원
|
||||
기록했으며, Q4에 최고치를 달성했습니다.
|
||||
|
||||
주요 성장요인:
|
||||
- 신제품 Pro-XL 출시로 인한 프리미엄 라인 매출 확대
|
||||
- 해외 수출 비중 15% ~ 28% 증가
|
||||
- 온라인 채널 전환으로 마진율 개선
|
||||
|
||||
4분기 실적은 연말 프로모션과 신규 파트너십 체결 효과로 큰 폭 상승했습니다.
|
||||
""",
|
||||
metadata = {
|
||||
"source": "annual_report_2024.txt",
|
||||
"type": "text",
|
||||
"category": "재무"
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
return docs
|
||||
|
||||
def image_to_caption(image_path):
|
||||
"""
|
||||
이미지를 설명 텍스트로 변환
|
||||
Vision LLM이 이미지를 분석하여 검색 간으한 텍스트 설명 생성
|
||||
"""
|
||||
img_b64 = encode_image(image_path)
|
||||
message = HumanMessage(
|
||||
content = [
|
||||
{"type":"image_url", "image_url":{'url':f'data:image/jpeg;base64,{img_b64}'}},
|
||||
{"type":"text", "text": """
|
||||
이 이미지를 검색용 설명문으로 요약하세요.
|
||||
200자 이내로 작성하세요.
|
||||
핵심 객체와 텍스트만 포함하세요.
|
||||
"""}
|
||||
]
|
||||
)
|
||||
return vision_llm.invoke([message]).content
|
||||
|
||||
def build_multimodal_index(image_dir:str, text_docs:list[Document]):
|
||||
"""
|
||||
이미지 폴더의 모든 이미지들을 캡셔닝하여 텍스트 문서와 함께 벡터 인덱스 구축
|
||||
"""
|
||||
all_docs = list(text_docs)
|
||||
|
||||
# image_dir 안 파일 가져오기
|
||||
# image_files = list(Path(image_dir).glob("*.{jpg, jpeg, png}"))
|
||||
valid_extensions = {".jpg", ".jpeg", ".png", ".webp", ".JPG", ".JPEG", ".PNG"}
|
||||
image_files = [
|
||||
p for p in Path(image_dir).rglob("*")
|
||||
if p.suffix in valid_extensions
|
||||
]
|
||||
|
||||
if not image_files:
|
||||
print(f"{image_dir}에 이미지를 찾지 못했습니다..")
|
||||
else:
|
||||
print(f"이미지 캡셔닝 {len(image_files)}개")
|
||||
|
||||
# 캡션 생성 => Document => 임베딩
|
||||
for img_path in image_files:
|
||||
caption = image_to_caption(image_path=img_path)
|
||||
|
||||
doc = Document(page_content=caption, metadata={
|
||||
"source": str(img_path),
|
||||
"type": "image",
|
||||
"image_path": str(img_path)
|
||||
})
|
||||
all_docs.append(doc)
|
||||
|
||||
# 기존 DB 삭제
|
||||
db_path = "./db/multimodal_db"
|
||||
if Path(db_path).exists():
|
||||
shutil.rmtree(db_path)
|
||||
|
||||
return Chroma.from_documents(all_docs, watson_embedding, persist_directory=db_path)
|
||||
|
||||
def search_with_images(vectorstore, query):
|
||||
"""
|
||||
벡터 검색 결과를 텍스트/이미지 결과로 분리하여 반환
|
||||
"""
|
||||
results = vectorstore.similarity_search_with_relevance_scores(query, k=5)
|
||||
text_results = [doc for doc, score in results if doc.metadata.get("type") != 'image']
|
||||
image_results = [doc for doc, score in results if doc.metadata.get("type") == 'image']
|
||||
print(f"텍스트 결과 : {len(text_results)}, 이미지 결과 {len(image_results)}")
|
||||
return text_results, image_results
|
||||
|
||||
def multimodal_answer(vectorstore, question):
|
||||
"""
|
||||
특스트 + 이미지 검색 결과를 통합하여 멀티모달 최종 답변 생성
|
||||
args:
|
||||
vectorstore: Chroma 벡터 스토어
|
||||
question: 사용자 질문
|
||||
|
||||
return "
|
||||
{"answer" : str, "images": list[srt]}
|
||||
"""
|
||||
text_results, image_results = search_with_images(vectorstore = vectorstore, query = question)
|
||||
|
||||
# 텍스트 결과 하나의 컨텍스트로 생성
|
||||
text_context = "\n\n".join([r.page_content for r in text_results])
|
||||
|
||||
# 이미지로 검색된 경우
|
||||
image_context = ""
|
||||
referenced_images = []
|
||||
for img_doc in image_results[:3]:
|
||||
img_path = img_doc.metadata.get("image_path")
|
||||
|
||||
img_b64 = encode_image(img_path)
|
||||
analysis = vision_llm.invoke([HumanMessage(
|
||||
content = [
|
||||
{"type":"image_url", "image_url":{'url':f'data:image/jpeg;base64,{img_b64}'}},
|
||||
{"type":"text", "text": f"이 이미지에서 다음 질문과 관련된 내용을 설명하세요: {question}"}
|
||||
])
|
||||
]).content
|
||||
|
||||
image_context += f"[이미지 분석: {img_path}]\n{analysis}\n\n"
|
||||
referenced_images.append(img_path)
|
||||
|
||||
# 최종 답변
|
||||
combined_context = text_context + "\n\n" + image_context
|
||||
|
||||
final_prompt = ChatPromptTemplate.from_messages([
|
||||
("system", "다음은 텍스트와 이미지 분석 결과를 참고하여 질문에 대한 답변입니다.\n\n{context}"),
|
||||
("human", "{question}")
|
||||
])
|
||||
|
||||
parser = StrOutputParser()
|
||||
chain = final_prompt | watson_llm | parser
|
||||
|
||||
answer = chain.invoke({
|
||||
"context": combined_context,
|
||||
"question": question
|
||||
})
|
||||
|
||||
return {"answer" : answer, "images": referenced_images}
|
||||
|
||||
|
||||
#########
|
||||
# 기존 벡터스토어 로드 함수
|
||||
#########
|
||||
|
||||
def load_or_build_vectorstore(image_dir, text_docs, db_path="./db/multimodal_db", force_rebuild=False):
|
||||
"""
|
||||
이미 벡터스토어가 존재하면 로드하고, 없으면 새로 구축
|
||||
"""
|
||||
if not force_rebuild and Path(db_path).exists():
|
||||
print(f"기존 멀티모달 DB 로드 완료: {db_path}")
|
||||
return Chroma(embedding_function=watson_embedding, persist_directory=db_path)
|
||||
|
||||
return build_multimodal_index(image_dir, text_docs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
IMAGE_DIR = "./sample_images"
|
||||
# 샘플 이미지 생성
|
||||
create_sample_images(IMAGE_DIR)
|
||||
# 샘플 텍스트 생성
|
||||
text_docs = create_sample_text_docs()
|
||||
# 벡터스토어(인덱스) 생성
|
||||
vectorstore = load_or_build_vectorstore(image_dir=IMAGE_DIR, text_docs=text_docs, force_rebuild=False)
|
||||
# vectorstore = load_or_build_vectorstore(IMAGE_DIR, text_docs, "./db", False)
|
||||
|
||||
print("\n")
|
||||
print("=" * 60)
|
||||
print("질의 응답 테스트")
|
||||
print("=" * 60)
|
||||
result = multimodal_answer(vectorstore, "매출 차트의 추세는?")
|
||||
print(result["answer"])
|
||||
|
||||
if result["images"]:
|
||||
for img_path in result["images"]:
|
||||
print(img_path)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user