1. 랭체인 이미지 인식 후 처리 마무리
2. fastAPI로 프로젝트 구조 실습 1. 랭체인 이미지 인식 후 처리 마무리 2. fastAPI로 프로젝트 구조 실습
This commit is contained in:
1 parent
d6be22857b
commit
ccfdac1286
59 files changed
+3144
-133
No files matched your search
Generated
+1
@@ -6,6 +6,7 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/ollama/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/ollama/.venv" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/qiskit/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/qiskit/.venv" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/project/rag_app/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="~/Source/.venv" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="~/Source/.venv" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
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$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</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" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</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>
|
||||||
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
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": {
|
"metadata": {
|
||||||
"collapsed": true,
|
"collapsed": true,
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:40:13.650962123Z",
|
"end_time": "2026-06-15T03:06:07.002963942Z",
|
||||||
"start_time": "2026-06-09T08:40:09.151285584Z"
|
"start_time": "2026-06-15T03:06:05.360626592Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"source": [
|
"source": [
|
||||||
"import base64\n",
|
"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 WatsonxEmbeddings\n",
|
||||||
"from langchain_ibm import ChatWatsonx\n",
|
"from langchain_ibm import ChatWatsonx\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -41,23 +44,14 @@
|
|||||||
"from langchain_core.documents import Document\n",
|
"from langchain_core.documents import Document\n",
|
||||||
"from langchain_core.messages import HumanMessage"
|
"from langchain_core.messages import HumanMessage"
|
||||||
],
|
],
|
||||||
"outputs": [
|
"outputs": [],
|
||||||
{
|
"execution_count": 3
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:40:13.666828279Z",
|
"end_time": "2026-06-15T03:06:07.012203786Z",
|
||||||
"start_time": "2026-06-09T08:40:13.654303979Z"
|
"start_time": "2026-06-15T03:06:07.003754555Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -71,13 +65,13 @@
|
|||||||
],
|
],
|
||||||
"id": "27bd39a0da002140",
|
"id": "27bd39a0da002140",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 2
|
"execution_count": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:40:17.037718935Z",
|
"end_time": "2026-06-15T03:06:09.243124960Z",
|
||||||
"start_time": "2026-06-09T08:40:13.668291706Z"
|
"start_time": "2026-06-15T03:06:07.013041549Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -107,7 +101,7 @@
|
|||||||
],
|
],
|
||||||
"id": "1b23141c1b60154a",
|
"id": "1b23141c1b60154a",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 3
|
"execution_count": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
@@ -118,8 +112,8 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:40:17.084968096Z",
|
"end_time": "2026-06-15T03:06:09.262869259Z",
|
||||||
"start_time": "2026-06-09T08:40:17.063355507Z"
|
"start_time": "2026-06-15T03:06:09.252393224Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -132,13 +126,13 @@
|
|||||||
],
|
],
|
||||||
"id": "637e09cdf9c78eee",
|
"id": "637e09cdf9c78eee",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 4
|
"execution_count": 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:40:17.236651215Z",
|
"end_time": "2026-06-15T03:06:09.329919223Z",
|
||||||
"start_time": "2026-06-09T08:40:17.091569162Z"
|
"start_time": "2026-06-15T03:06:09.264035014Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -168,13 +162,13 @@
|
|||||||
],
|
],
|
||||||
"id": "a1114bd96f354025",
|
"id": "a1114bd96f354025",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 5
|
"execution_count": 7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:41:42.833366570Z",
|
"end_time": "2026-06-15T03:06:21.590918755Z",
|
||||||
"start_time": "2026-06-09T08:41:20.436085763Z"
|
"start_time": "2026-06-15T03:06:09.330837660Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -185,25 +179,30 @@
|
|||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"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
|
"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",
|
"cell_type": "code",
|
||||||
"source": "#### 2. URL 이미지",
|
"source": "#### 2. URL 이미지",
|
||||||
"id": "79877a4f0166e70d",
|
"id": "79877a4f0166e70d",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": null
|
"execution_count": 9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:42:15.301506904Z",
|
"end_time": "2026-06-15T03:06:57.034661130Z",
|
||||||
"start_time": "2026-06-09T08:41:45.482282423Z"
|
"start_time": "2026-06-15T03:06:21.613674680Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -233,34 +232,34 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"text/plain": [
|
"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": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"execution_count": 9
|
"execution_count": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:42:15.314335059Z",
|
"end_time": "2026-06-15T03:06:57.059273077Z",
|
||||||
"start_time": "2026-06-09T08:42:15.303693807Z"
|
"start_time": "2026-06-15T03:06:57.045190160Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": "#### 3. OCR",
|
"source": "#### 3. OCR",
|
||||||
"id": "9549854d2a4d9c89",
|
"id": "9549854d2a4d9c89",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 10
|
"execution_count": 11
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:42:15.335585069Z",
|
"end_time": "2026-06-15T03:06:57.080602805Z",
|
||||||
"start_time": "2026-06-09T08:42:15.316787126Z"
|
"start_time": "2026-06-15T03:06:57.060282470Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -275,13 +274,13 @@
|
|||||||
],
|
],
|
||||||
"id": "32f3f6d75760fcec",
|
"id": "32f3f6d75760fcec",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 11
|
"execution_count": 12
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:49:33.136027357Z",
|
"end_time": "2026-06-15T03:07:15.435586589Z",
|
||||||
"start_time": "2026-06-09T08:49:09.992205257Z"
|
"start_time": "2026-06-15T03:06:57.083111604Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -297,7 +296,7 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"message = HumanMessage(\n",
|
"message = HumanMessage(\n",
|
||||||
" content = [\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",
|
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
|
||||||
"\n",
|
"\n",
|
||||||
" ]\n",
|
" ]\n",
|
||||||
@@ -315,50 +314,58 @@
|
|||||||
"# 문서 텍스트 추출\n",
|
"# 문서 텍스트 추출\n",
|
||||||
"\n",
|
"\n",
|
||||||
"## 미래로봇추진단(서울 근무)\n",
|
"## 미래로봇추진단(서울 근무)\n",
|
||||||
"### S/W개발 - 시스템 소프트웨어\n",
|
"### S/W개발 - 시스템/소프트웨어\n",
|
||||||
"\n",
|
"\n",
|
||||||
"---\n",
|
"---\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 포지션 소개 (Job Overview)\n",
|
"**포지션 소개 (Job Overview)**\n",
|
||||||
"휴머노이드 로봇의 실시간 제어 시스템 및 소프트웨어 플랫폼을 개발하는 직무입니다. 운영체제 환경 구성, 디바이스 드라이버, 실시간 제어 프레임워크 등 로봇 동작의 핵심 기반이 되는 소프트웨어를 설계 및 개발하며, 하드웨어 설계 조직 및 AI 연구 조직과 긴밀히 협업합니다.\n",
|
"휴머노이드 로봇의 실시간 제어 시스템 및 소프트웨어 플랫폼을 개발하는 직무입니다. 운영체제 환경 구성, 디바이스 드라이버, 실시간 제어 프레임워크 등 로봇 동작의 핵심 기반이 되는 소프트웨어를 설계 및 개발하며, 하드웨어 설계 조직 및 AI 연구 조직과 긴밀히 협업합니다.\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 수행업무 (Job Details)\n",
|
"---\n",
|
||||||
|
"\n",
|
||||||
|
"**수행업무 (Job Details)**\n",
|
||||||
"- 휴머노이드 로봇의 실시간 제어 프레임워크를 설계하고 개발합니다.\n",
|
"- 휴머노이드 로봇의 실시간 제어 프레임워크를 설계하고 개발합니다.\n",
|
||||||
"- 로봇, 센서 등 로봇 하드웨어 제어를 위한 디바이스 드라이버 및 하드웨어 추상화 계층을 개발합니다.\n",
|
"- 로보, 센서 등 로봇 하드웨어 제어를 위한 디바이스 드라이버 및 하드웨어 추상화 계층을 개발합니다.\n",
|
||||||
"- 로봇 제어를 운영체제(Linux 기반 실시간 OS 등) 환경을 구성하고 시스템 성능을 최적화합니다.\n",
|
"- 로봇 제어를 운영체제(Linux 기반 실시간 OS 등) 환경에 구성하고 시스템 성능을 최적화합니다.\n",
|
||||||
"- 유관 부서와 협업하여 로봇 조직 등 시 기능과 제어 시스템 간 연동 미들웨어를 개발합니다.\n",
|
"- 유관 부서와 협업하여 로봇 조직 등 시 기능과 제어 시스템 간 연동 미들웨어를 개발합니다.\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 자격요건 (Requirements)\n",
|
"---\n",
|
||||||
|
"\n",
|
||||||
|
"**자격요건 (Requirements)**\n",
|
||||||
"- 컴퓨터, 전기·전자, 기계, 로봇공학 등 관련 전공을 하신 분\n",
|
"- 컴퓨터, 전기·전자, 기계, 로봇공학 등 관련 전공을 하신 분\n",
|
||||||
"- 운영체제 기반 개념에 대한 이해도를 보유하신 분\n",
|
"- 운영체제 기반 개념에 대한 이해도를 보유하신 분\n",
|
||||||
"- 요구사항을 분석하여 소프트웨어를 구조적으로 설계 및 구현하는 역량을 보유하신 분\n",
|
"- 요구사항을 분석하여 소프트웨어를 구조적으로 설계 및 구현하는 역량을 보유하신 분\n",
|
||||||
"- 다양한 분야의 엔지니어와 적극적으로 소통하며 협업할 수 있는 역량을 보유하신 분\n",
|
"- 다양한 분야의 엔지니어와 적극적으로 소통하며 협업할 수 있는 역량을 보유하신 분\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 우대사항 (Preferences)\n",
|
"---\n",
|
||||||
|
"\n",
|
||||||
|
"**우대사항 (Preferences)**\n",
|
||||||
"- C/C++ 기반 시스템 프로그래밍 역량을 보유하신 분\n",
|
"- C/C++ 기반 시스템 프로그래밍 역량을 보유하신 분\n",
|
||||||
"- Linux 기반 임베디드 시스템 개발 경험을 보유하신 분 (Kernel, Device Driver, BSP 등)\n",
|
"- Linux 기반 임베디드 시스템 개발 경험을 보유하신 분 (Kernel, Device Driver, BSP 등)\n",
|
||||||
"- CAN, EtherCAT, UDP 등 하드웨어 통신 프로토콜 활용 경험을 보유하신 분\n",
|
"- CAN, EtherCAT, UDP 등 하드웨어 통신 프로토콜 활용 경험을 보유하신 분\n",
|
||||||
"- ROS2 기반 로봇제어 소프트웨어 수행 경험을 보유하신 분\n",
|
"- ROS2 기반 로봇제어 소프트웨어 수행 경험을 보유하신 분\n",
|
||||||
"- Git 기반 협업 및 CI/CD 환경에서의 개발 경험을 보유하신 분\n",
|
"- Git 기반 협업 및 CI/CD 환경에서의 개발 경험을 보유하신 분\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 커리어 비전 (Career Vision)\n",
|
"---\n",
|
||||||
"휴머노이드 로봇의 초기 개발 단계부터 참여하여 핵심 개발자로 성장할 수 있습니다. 실시간 제어, 시스템 아키텍처, 로봇 미들웨어 등 다양한 경험을 통해 로봇제어 시스템 SW 전문가, 나아가 시스템 아키텍트로의 커리어 확장이 가능합니다.\n",
|
"\n",
|
||||||
|
"**커리어 비전 (Career Vision)**\n",
|
||||||
|
"휴머노이드 로봇의 초기 개발 단계부터 참여하여 핵심 개발자로 성장할 수 있습니다. 실시간 제어, 시스템 아키텍처, 로봇 미들웨어 등 다양한 경험을 통해 로봇틱스 시스템 SW 전문가, 나아가 시스템 아키텍트로의 커리어 확장이 가능합니다.\n",
|
||||||
"\n",
|
"\n",
|
||||||
"---\n",
|
"---\n",
|
||||||
"\n",
|
"\n",
|
||||||
"> **\"휴머노이드의 미래를 함께 실현할 분을 찾습니다\"**\n",
|
"> **\"휴머노이드의 미래를 함께 설계할 분을 찾습니다\"**\n",
|
||||||
"> \n",
|
"> \n",
|
||||||
"> 로봇이 인공적으로 동작하기 위한 소프트웨어 기반을 함께 만들어 가며, 도전적인 기술 과제를 즐기는 분의 지원을 환영합니다.\n"
|
"> 로봇이 인정적으로 동작하기 위한 소프트웨어 기반을 함께 만들어가자. 도전적인 기술 과제를 즐기는 분의 지원을 환영합니다.\n"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"execution_count": 17
|
"execution_count": 13
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:58:04.822594430Z",
|
"end_time": "2026-06-15T03:07:15.454781421Z",
|
||||||
"start_time": "2026-06-09T08:58:04.802857234Z"
|
"start_time": "2026-06-15T03:07:15.444138572Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -381,13 +388,13 @@
|
|||||||
],
|
],
|
||||||
"id": "d184430da51ccc36",
|
"id": "d184430da51ccc36",
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"execution_count": 20
|
"execution_count": 14
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:58:41.448285681Z",
|
"end_time": "2026-06-15T03:07:32.850368373Z",
|
||||||
"start_time": "2026-06-09T08:58:04.941406663Z"
|
"start_time": "2026-06-15T03:07:15.455557487Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -401,68 +408,67 @@
|
|||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"# 두 제품의 비교 분석\n",
|
"# 두 제품 비교 분석\n",
|
||||||
"\n",
|
"\n",
|
||||||
"## 📦 제품 1: 냉장고 (이미지 1)\n",
|
"## 🖼️ 이미지 1: 냉장고 (4도어 프렌치 도어형)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"| 항목 | 내용 |\n",
|
"### 주요 특징\n",
|
||||||
"|------|------|\n",
|
"- **구조**: 4도어 프렌치 도어 디자인 (상단 2개 + 하단 2개)\n",
|
||||||
"| **제품 유형** | 4도어 프렌치 도어 냉장고 |\n",
|
"- **색상**: 화이트 / 크림색\n",
|
||||||
"| **외관 색상** | 화이트/크림 베이지 톤 |\n",
|
"- **디자인**: 미니멀하고 깔끔한 라인\n",
|
||||||
"| **구조** | 상단 2도어 + 하단 2도어 (중앙 검은색 손잡이 바) |\n",
|
"- **디스플레이**: 중앙에 검은색 디지털 디스플레이 패널\n",
|
||||||
"| **용도** | 식료품 보관 (냉동/냉장) |\n",
|
"- **바퀴**: 하단 조절 가능한 발\n",
|
||||||
"| **소재** | 금속 외관, 매트한 마감 |\n",
|
|
||||||
"| **공간 배치** | 부엌에 독립적으로 설치 |\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"## 🍽️ 제품 2: 식탁/테이블 (이미지 2)\n",
|
"### 용도\n",
|
||||||
"\n",
|
"주방용 대형 가전제품 (식품 보관)\n",
|
||||||
"| 항목 | 내용 |\n",
|
|
||||||
"|------|------|\n",
|
|
||||||
"| **제품 유형** | 4인용 다용도 식탁 (다이닝 테이블) |\n",
|
|
||||||
"| **외관 색상** | 화이트 대리석 무늬 상판 + 블랙/골드 다리 |\n",
|
|
||||||
"| **구조** | 직사각형 상판 + 4개의 슬림 다리 |\n",
|
|
||||||
"| **용도** | 식사, 작업, 다용도 테이블 |\n",
|
|
||||||
"| **소재** | 석재/대리석 패턴 (MDF 또는 천연석 추정) + 금속 다리 |\n",
|
|
||||||
"| **공간 배치** | 거실/다이닝 공간에 배치 |\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"---\n",
|
"---\n",
|
||||||
"\n",
|
"\n",
|
||||||
"## 🔍 핵심 차이점 비교\n",
|
"## 🖼️ 이미지 2: 식탁 / 다이닝 테이블\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 1️⃣ **기능적 차이**\n",
|
"### 주요 특징\n",
|
||||||
"- **냉장고**: 식품 보관·냉각이라는 **단일 기능성 가전**\n",
|
"- **소재**: 대리석 (화이트 + 그레이 veins) 상판\n",
|
||||||
"- **식탁**: 식사·작업·소통이 이루어지는 **생활 중심 가구**\n",
|
"- **다리**: 검은색/다크 컬러 + 골드(Gold) 팁\n",
|
||||||
|
"- **형태**: 직사각형, 라운드 코너 처리\n",
|
||||||
|
"- **구조**: 모던한 4각 다리형\n",
|
||||||
|
"- **수납**: 하단 선반 구조\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 2️⃣ **디자인 철학**\n",
|
"### 용도\n",
|
||||||
"- **냉장고**: 미니멀하고 깔끔한 모노톤, 현대 주방 인테리어에 맞춤\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",
|
||||||
|
"| **용도** | 식품 보관·냉각 | 식사·작업 공간 |\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": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
"end_time": "2026-06-09T08:59:26.775983373Z",
|
"end_time": "2026-06-15T03:08:00.503202012Z",
|
||||||
"start_time": "2026-06-09T08:58:56.843217983Z"
|
"start_time": "2026-06-15T03:07:32.869952272Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@@ -480,57 +486,328 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"## 📊 차트 1: 최근 6개월 전체 화장품 수출 추이\n",
|
"## 📊 차트 1: 최근 6개월 전체 화장품 수출 추이\n",
|
||||||
"\n",
|
"\n",
|
||||||
"| 기간 | 수출액(억 달러) | YoY 증감률 |\n",
|
"| 기간 | 수출액(백만$) | YoY 증감률 |\n",
|
||||||
"|------|----------------|-----------|\n",
|
"|------|---------------|------------|\n",
|
||||||
"| 25년 12월 | 883.7 | +10.2% |\n",
|
"| 25년 12월 | 883.7 | +10.2% |\n",
|
||||||
"| 26년 1월 | 841.6 | **+33.7%** (최고점) |\n",
|
"| 26년 1월 | 841.6 | **+33.7%** (최고) |\n",
|
||||||
"| 26년 2월 | 752.1 | +1.7% (최저점) |\n",
|
"| 26년 2월 | 752.1 | +1.7% (최저) |\n",
|
||||||
"| 26년 3월 | 960.4 | +23.0% |\n",
|
"| 26년 3월 | 960.4 | +23.0% |\n",
|
||||||
"| 26년 4월 | **1,096.3** (최고 수출액) | +21.7% |\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",
|
"\n",
|
||||||
"## 📊 차트 2: 5월 1~20일 주요 국가별 일평균 수출액\n",
|
"## 📊 차트 2: 5월 1~20일 주요 국가별 일평균 수출액\n",
|
||||||
"\n",
|
"\n",
|
||||||
"| 국가 | 일평균 수출액(백만 달러) | YoY 증감률 |\n",
|
"| 국가 | 일평균 수출액(백만$) | YoY 증감률 |\n",
|
||||||
"|------|-------------------------|-----------|\n",
|
"|------|---------------------|------------|\n",
|
||||||
"| 중화권(중국) | 13.13 | **+76.4%** |\n",
|
"| 중화권(중국) | 13.13 | **+76.4%** (최고) |\n",
|
||||||
"| 유럽 | 11.14 | +61.3% |\n",
|
|
||||||
"| 미국 | 11.66 | +40.3% |\n",
|
"| 미국 | 11.66 | +40.3% |\n",
|
||||||
|
"| 유럽 | 11.14 | +61.3% |\n",
|
||||||
"| 동남아 | 4.91 | +22.0% |\n",
|
"| 동남아 | 4.91 | +22.0% |\n",
|
||||||
"| 일본 | 약 3.10 | **-14.1%** |\n",
|
"| 일본 | 3.xx | **-14.1%** (유일한 마이너스) |\n",
|
||||||
"\n",
|
"\n",
|
||||||
"---\n",
|
"---\n",
|
||||||
"\n",
|
"\n",
|
||||||
"## 🔍 주요 변화 추이 분석\n",
|
"## 🔍 주요 변화 추이 분석\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 1. **4월까지 성장 → 5월 급격한 반전**\n",
|
"### 1️⃣ **전체 수출의 5월 급격한 반전**\n",
|
||||||
"- 1~4월 동안 꾸준한 성장세(YoY +21~34%)를 유지하며 4월에 **사상 최고치 1,096.3억 달러**를 기록\n",
|
"- 1~4월은 두 자릿수 성장(YoY +20~33%)을 유지하며 **우상향 흐름**이었으나, 5월(1~20일) 들어 **-16% 마이너스 성장**으로 전환\n",
|
||||||
"- 그러나 5월(1~20일)에 들어 **YoY -16.0%로 마이너스 전환**되며 성장세 급격히 둔화\n",
|
"- 4월 대비 약 425백만$ 감소하며 **6개월 중 최저 수출액** 기록\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 2. **국가별 양극화 현상**\n",
|
"### 2️⃣ **국가별 양극화 현상**\n",
|
||||||
"- **📈 신흥 시장 강세**: 중화권(+76.4%), 유럽(+61.3%), 미국(+40.3%) 등 주요 시장에서 고른 성장\n",
|
"- ✅ **호조**: 중화권(76.4%), 유럽(61.3%), 미국(40.3%) 등 신흥·대형 시장은 여전히 고성장 지속\n",
|
||||||
"- **📉 일본 시장 침체**: 유일하게 마이너스(-14.1%)를 기록하며 전체 수출액 둔화의 주요 원인으로 작용\n",
|
"- ❌ **악화**: 일본은 유일한 마이너스(-14.1%) 국가로, **일·중 갈등 리스크**(수출 규제 등)가 직접적 영향으로 작용한 것으로 추정\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 3. **성장 동력의 재편**\n",
|
"### 3️⃣ **시장 다변화 효과**\n",
|
||||||
"- 종전에는 일본이 주요 화장품 수출 대상국이었으나, 일본 시장 위축이 전체 지표에 영향\n",
|
"- 5월 전체 수출 급감에도 **중국·유럽·미국** 비중이 절대적(1일 평균 11백만$ 이상)이라, 일본 위축이 전체 실적을 끌어내린 구조\n",
|
||||||
"- **중화권·유럽·미국 중심의 수출 구조 재편**이 가속화되는 흐름\n",
|
"- 중화권 일평균 수출액(13.13M$)이 전체 5월 실적의 약 **39%**(671.1M$ ÷ 20일 = 33.6M$ 기준)를 차지\n",
|
||||||
"\n",
|
"\n",
|
||||||
"### 4. **시사점**\n",
|
"### 4️⃣ **핵심 인사이트**\n",
|
||||||
"- 5월 마이너스 전환은 **일본 시장 감소**가 가장 큰 영향 요인\n",
|
"> 📌 **\"성장 동력은 견고하나, 지정학적 리스크 노출도 확대\"**\n",
|
||||||
"- 단, 5월은 1~20일(부분 집계) 기간이므로 **후반부 회복 여부** 추가 모니터링 필요\n",
|
"> - 美·유럽·중국의 K-뷰티 수요는 여전히 강력\n",
|
||||||
"- 한·중·일 관계 변화, 환율, 관세 등 외부 변수 영향 지속 점검 요구됨\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": {},
|
"metadata": {},
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"outputs": [],
|
|
||||||
"execution_count": null,
|
|
||||||
"source": "",
|
"source": "",
|
||||||
"id": "972db1f4ffac7c6f"
|
"id": "bce01c1bf17b40e1",
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"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 |
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# 쿼리 파일을 포함한 무시된 디폴트 폴더
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="~/Source/project/.venv" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/project.iml" filepath="$PROJECT_DIR$/.idea/project.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/rag_app/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="~/Source/project/.venv" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PyToolsState">
|
||||||
|
<option name="tools">
|
||||||
|
<map>
|
||||||
|
<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>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 샘플 Python 스크립트입니다.
|
||||||
|
|
||||||
|
# Shift+F10을(를) 눌러 실행하거나 내 코드로 바꿉니다.
|
||||||
|
# 클래스, 파일, 도구 창, 액션 및 설정을 어디서나 검색하려면 Shift 두 번을(를) 누릅니다.
|
||||||
|
|
||||||
|
|
||||||
|
def print_hi(name):
|
||||||
|
# 스크립트를 디버그하려면 하단 코드 줄의 중단점을 사용합니다.
|
||||||
|
print(f'Hi, {name}') # 중단점을 전환하려면 Ctrl+F8을(를) 누릅니다.
|
||||||
|
|
||||||
|
|
||||||
|
# 스크립트를 실행하려면 여백의 녹색 버튼을 누릅니다.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print_hi('PyCharm')
|
||||||
|
|
||||||
|
# https://www.jetbrains.com/help/pycharm/에서 PyCharm 도움말 참조
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# 쿼리 파일을 포함한 무시된 디폴트 폴더
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PortForwardingSettings">
|
||||||
|
<ports>
|
||||||
|
<entry key="8000">
|
||||||
|
<ForwardedPortInfo>
|
||||||
|
<option name="hostPort" value="8000" />
|
||||||
|
<option name="readOnly" value="false" />
|
||||||
|
</ForwardedPortInfo>
|
||||||
|
</entry>
|
||||||
|
</ports>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="~/Source/project/rag_app/.venv" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/rag_app.iml" filepath="$PROJECT_DIR$/.idea/rag_app.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
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>
|
||||||
Generated
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="~/Source/project/rag_app/.venv" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from langchain_ibm import WatsonxEmbeddings
|
||||||
|
from backend.config.settings import settings
|
||||||
|
|
||||||
|
watson_embedding = WatsonxEmbeddings(
|
||||||
|
model_id="ibm/granite-embedding-278m-multilingual",
|
||||||
|
url=f"{settings.watsonx_url}",
|
||||||
|
api_key=f"{settings.watsonx_api_key}",
|
||||||
|
project_id=f"{settings.watsonx_project_id}",
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from langchain_ibm import ChatWatsonx
|
||||||
|
from backend.config.settings import settings
|
||||||
|
|
||||||
|
watson_llm = ChatWatsonx(
|
||||||
|
model_id="ibm/granite-4-h-small",
|
||||||
|
url=f"{settings.watsonx_url}",
|
||||||
|
api_key=f"{settings.watsonx_api_key}",
|
||||||
|
project_id=f"{settings.watsonx_project_id}",
|
||||||
|
max_tokens=2000,
|
||||||
|
params={
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from pydantic import Field
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_file="backend/.env", extra="ignore")
|
||||||
|
# 사용할 모델
|
||||||
|
watsonx_api_key: str = Field(alias="WATSONX_API_KEY")
|
||||||
|
watsonx_project_id: str = Field(alias="WATSONX_PROJECT_ID")
|
||||||
|
watsonx_url: str = Field(alias="WATSONX_URL")
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
from backend.routers.page_router import router as page_router
|
||||||
|
from backend.routers.api_router import router as api_router
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.include_router(page_router)
|
||||||
|
app.include_router(api_router)
|
||||||
|
|
||||||
|
# static 폴더 지정
|
||||||
|
app.mount("/static", StaticFiles(directory="backend/static"), name="static")
|
||||||
|
|
||||||
|
# http://127.0.0.1:8000/item/1 + GET
|
||||||
|
# @app.get("/item/{item_id}")
|
||||||
|
# async def read_item(item_id):
|
||||||
|
# return {"item_id" : item_id}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from fastapi import APIRouter, Request, UploadFile
|
||||||
|
from backend.services.llm_service import question_and_answer
|
||||||
|
from backend.schemas.basic_schema import QuestionRequest
|
||||||
|
from backend.services.rag_service import upload_document
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
# http://127.0.0.1:8000/api/question
|
||||||
|
@router.post("/question")
|
||||||
|
async def question(req:QuestionRequest):
|
||||||
|
answer = question_and_answer(req.question)
|
||||||
|
|
||||||
|
return {"message" : answer}
|
||||||
|
|
||||||
|
# http://127.0.0.1:8000/api/rag/upload
|
||||||
|
@router.post("/rag/upload")
|
||||||
|
async def fileUpload(file:UploadFile):
|
||||||
|
# 서비스 호출
|
||||||
|
return upload_document(file)
|
||||||
|
|
||||||
|
|
||||||
|
# http://127.0.0.1:8000/api/rag/question
|
||||||
|
|
||||||
|
@router.post("/rag/question")
|
||||||
|
async def question():
|
||||||
|
pass
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
templates = Jinja2Templates(directory="backend/templates")
|
||||||
|
|
||||||
|
# http://127.0.0.1:8000
|
||||||
|
@router.get("/")
|
||||||
|
async def home(request : Request):
|
||||||
|
return templates.TemplateResponse(request = request, name="index.html")
|
||||||
|
|
||||||
|
@router.get("/rag")
|
||||||
|
async def rag(request : Request):
|
||||||
|
return templates.TemplateResponse(request = request, name="rag.html")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class QuestionRequest(BaseModel):
|
||||||
|
question : str
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from backend.ai.llm import watson_llm
|
||||||
|
|
||||||
|
# LLM 모델 통신
|
||||||
|
# 데이터베이스 통신
|
||||||
|
|
||||||
|
def question_and_answer(question):
|
||||||
|
response = watson_llm.invoke(question)
|
||||||
|
return response.content
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# pdf 업로드 => 분할 => 인덱스 생성
|
||||||
|
def upload_document(file):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# 질문 => 유사도 검색 => 문서 => llm 답변 생성
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
document.querySelector("button").addEventListener("click", ask)
|
||||||
|
|
||||||
|
async function ask() {
|
||||||
|
// 사용자가 질문 입력 시 질문을 서버로 전송
|
||||||
|
const question = document.querySelector('#question').value
|
||||||
|
|
||||||
|
const response = await fetch("/api/question", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({question : question})
|
||||||
|
})
|
||||||
|
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
||||||
|
const answer = await response.json()
|
||||||
|
document.querySelector('#answer').textContent = answer.message
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 업로드
|
||||||
|
document.querySelector("#uploadBtn").addEventListener("click", uploadFile)
|
||||||
|
async function uploadFile()
|
||||||
|
{
|
||||||
|
const fileInput = document.querySelector("#file");
|
||||||
|
// 첨부파일 정보 가져오기
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
if(!file)
|
||||||
|
{
|
||||||
|
alert("파일을 선택해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// form 만들어 전송
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
|
||||||
|
const response = await fetch("/api/reg/question", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
||||||
|
const answer = await response.json()
|
||||||
|
document.querySelector('#answer').textContent = answer.message
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>HOME</h1>
|
||||||
|
<ul>
|
||||||
|
<a href="/rag">RAG</a>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<input type="text" name="question" id="question">
|
||||||
|
<button type="submit">질문</button>
|
||||||
|
</div>
|
||||||
|
<div id="answer"></div>
|
||||||
|
<script src="{{ url_for('static', path='js/index.js')}}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>RAG</h2>
|
||||||
|
<div>
|
||||||
|
<input type="file" name="file" id="file">
|
||||||
|
<button id="uploadBtn">업로드</button>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div id="result"></div>
|
||||||
|
<script src="{{ url_for('static', path='js/index.js')}}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[project]
|
||||||
|
name = "rag-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rag-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# 쿼리 파일을 포함한 무시된 디폴트 폴더
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/pythonSource.iml" filepath="$PROJECT_DIR$/.idea/pythonSource.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PackageRequirementsSettings" />
|
||||||
|
<component name="ReSTService" />
|
||||||
|
</module>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+5
@@ -3,6 +3,11 @@
|
|||||||
<component name="PyToolsState">
|
<component name="PyToolsState">
|
||||||
<option name="tools">
|
<option name="tools">
|
||||||
<map>
|
<map>
|
||||||
|
<entry key="black">
|
||||||
|
<value>
|
||||||
|
<ToolEntry />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
<entry key="pyrefly">
|
<entry key="pyrefly">
|
||||||
<value>
|
<value>
|
||||||
<ToolEntry />
|
<ToolEntry />
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user