Spaces:
Running
Running
update chromaDB
Browse files- app.py +9 -6
- backend/api.py +1 -1
- chains/__pycache__/rag.cpython-312.pyc +0 -0
- chains/rag.py +0 -65
- chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/data_level0.bin +3 -0
- chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/header.bin +3 -0
- chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/length.bin +3 -0
- chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/link_lists.bin +0 -0
- chroma_data/chroma.sqlite3 +3 -0
- db_setup.py +91 -0
- src/chains/__pycache__/rag.cpython-312.pyc +0 -0
- src/chains/rag.py +83 -0
app.py
CHANGED
|
@@ -5,7 +5,10 @@ import cv2
|
|
| 5 |
import torch
|
| 6 |
import tempfile
|
| 7 |
import os
|
| 8 |
-
from chains.rag import generate_narrative
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
st.set_page_config(page_title="ourchamplie", layout="centered")
|
|
@@ -14,7 +17,7 @@ st.title("🌿Aplikasi Pendeteksi Penyakit Daun Cabai")
|
|
| 14 |
|
| 15 |
@st.cache_resource
|
| 16 |
def load_model():
|
| 17 |
-
model = YOLO("model/
|
| 18 |
return model
|
| 19 |
|
| 20 |
model = load_model()
|
|
@@ -22,7 +25,7 @@ uploaded_file = st.file_uploader("Unggah gambar daun cabai anda", type=["jpg", "
|
|
| 22 |
|
| 23 |
if uploaded_file is not None:
|
| 24 |
image = Image.open(uploaded_file)
|
| 25 |
-
st.image(image, caption="Gambar yang Diupload", use_container_width=True)
|
| 26 |
|
| 27 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_file:
|
| 28 |
image.save(temp_file.name)
|
|
@@ -48,14 +51,14 @@ if uploaded_file is not None:
|
|
| 48 |
detected_labels[label] = confidence
|
| 49 |
|
| 50 |
for label, confidence in detected_labels.items():
|
| 51 |
-
st.markdown(f"
|
| 52 |
|
| 53 |
|
| 54 |
|
| 55 |
-
with st.spinner("
|
| 56 |
narrative = generate_narrative(label.capitalize())
|
| 57 |
|
| 58 |
-
st.subheader("
|
| 59 |
st.write(narrative)
|
| 60 |
|
| 61 |
os.remove(temp_path)
|
|
|
|
| 5 |
import torch
|
| 6 |
import tempfile
|
| 7 |
import os
|
| 8 |
+
from src.chains.rag import generate_narrative
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
os.environ["TRANSFORMERS_VERBOSITY"] = "error"
|
| 12 |
|
| 13 |
|
| 14 |
st.set_page_config(page_title="ourchamplie", layout="centered")
|
|
|
|
| 17 |
|
| 18 |
@st.cache_resource
|
| 19 |
def load_model():
|
| 20 |
+
model = YOLO("model/bestv11.pt")
|
| 21 |
return model
|
| 22 |
|
| 23 |
model = load_model()
|
|
|
|
| 25 |
|
| 26 |
if uploaded_file is not None:
|
| 27 |
image = Image.open(uploaded_file)
|
| 28 |
+
# st.image(image, caption="Gambar yang Diupload", use_container_width=True)
|
| 29 |
|
| 30 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_file:
|
| 31 |
image.save(temp_file.name)
|
|
|
|
| 51 |
detected_labels[label] = confidence
|
| 52 |
|
| 53 |
for label, confidence in detected_labels.items():
|
| 54 |
+
st.markdown(f"Hasil:{label.capitalize()} (skor: {confidence:.2f})")
|
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
+
with st.spinner("Menyusun penjelasan..."):
|
| 59 |
narrative = generate_narrative(label.capitalize())
|
| 60 |
|
| 61 |
+
st.subheader("Penjelasan")
|
| 62 |
st.write(narrative)
|
| 63 |
|
| 64 |
os.remove(temp_path)
|
backend/api.py
CHANGED
|
@@ -4,7 +4,7 @@ from ultralytics import YOLO
|
|
| 4 |
from PIL import Image
|
| 5 |
import io
|
| 6 |
import uvicorn
|
| 7 |
-
from chains.rag import generate_narrative
|
| 8 |
|
| 9 |
app = FastAPI(
|
| 10 |
title="ChiliCare API",
|
|
|
|
| 4 |
from PIL import Image
|
| 5 |
import io
|
| 6 |
import uvicorn
|
| 7 |
+
from src.chains.rag import generate_narrative
|
| 8 |
|
| 9 |
app = FastAPI(
|
| 10 |
title="ChiliCare API",
|
chains/__pycache__/rag.cpython-312.pyc
DELETED
|
Binary file (2.08 kB)
|
|
|
chains/rag.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
-
from langchain_openai import ChatOpenAI
|
| 4 |
-
from langchain_core.prompts import PromptTemplate
|
| 5 |
-
|
| 6 |
-
# 1. Inisialisasi LLM
|
| 7 |
-
llm = ChatOpenAI(
|
| 8 |
-
base_url="https://openrouter.ai/api/v1",
|
| 9 |
-
api_key=os.getenv("OPENROUTER_API_KEY"),
|
| 10 |
-
model="nvidia/nemotron-3-nano-30b-a3b:free",
|
| 11 |
-
temperature=0.6,
|
| 12 |
-
max_tokens=1200
|
| 13 |
-
)
|
| 14 |
-
|
| 15 |
-
def retrieve_disease_info(disease_name):
|
| 16 |
-
with open("data/data.json", "r", encoding="utf-8") as f:
|
| 17 |
-
data = json.load(f)
|
| 18 |
-
return data.get(disease_name)
|
| 19 |
-
|
| 20 |
-
# 2. Setup Prompt Template
|
| 21 |
-
prompt_template = PromptTemplate(
|
| 22 |
-
input_variables=["disease_name", "penyebab", "gejala", "penanganan", "pencegahan"],
|
| 23 |
-
template="""
|
| 24 |
-
Anda adalah penyuluh pertanian cabai.
|
| 25 |
-
|
| 26 |
-
Gunakan HANYA informasi berikut.
|
| 27 |
-
Jangan menambahkan fakta baru.
|
| 28 |
-
|
| 29 |
-
Nama penyakit: {disease_name}
|
| 30 |
-
|
| 31 |
-
Penyebab:
|
| 32 |
-
- {penyebab}
|
| 33 |
-
|
| 34 |
-
Gejala:
|
| 35 |
-
- {gejala}
|
| 36 |
-
|
| 37 |
-
Penanganan:
|
| 38 |
-
- {penanganan}
|
| 39 |
-
|
| 40 |
-
Pencegahan:
|
| 41 |
-
- {pencegahan}
|
| 42 |
-
|
| 43 |
-
Gunakan bahasa sederhana untuk petani cabai.
|
| 44 |
-
Jika penyakit adalah sehat, jelaskan bahwa daun dalam kondisi sehat.
|
| 45 |
-
"""
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
# 3. Gunakan LCEL (LangChain Expression Language) sebagai pengganti LLMChain
|
| 49 |
-
chain = prompt_template | llm
|
| 50 |
-
|
| 51 |
-
def generate_narrative(disease_name):
|
| 52 |
-
info = retrieve_disease_info(disease_name)
|
| 53 |
-
if not info:
|
| 54 |
-
return "⚠️ Data penyakit tidak ditemukan."
|
| 55 |
-
|
| 56 |
-
# 4. Gunakan .invoke() dan ambil kontennya
|
| 57 |
-
response = chain.invoke({
|
| 58 |
-
"disease_name": disease_name,
|
| 59 |
-
"penyebab": "; ".join(info["penyebab"]),
|
| 60 |
-
"gejala": "; ".join(info["gejala"]),
|
| 61 |
-
"penanganan": "; ".join(info["penanganan"]),
|
| 62 |
-
"pencegahan": "; ".join(info["pencegahan"]),
|
| 63 |
-
})
|
| 64 |
-
|
| 65 |
-
return response.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/data_level0.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:99acbc5e8493d5faef12f11e46065ecfc51823f6315e867c82b0fb04007dd629
|
| 3 |
+
size 423600
|
chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/header.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bf12d4486518c7addf488cb4854526902c78e91951990e1e2f4e055cec814e5d
|
| 3 |
+
size 100
|
chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/length.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d2ee7cb82599c809ee1967a459779fe297094941684fd31ae0805324957f654d
|
| 3 |
+
size 400
|
chroma_data/1537bc7a-6565-4b48-abe9-26f5ab013a16/link_lists.bin
ADDED
|
File without changes
|
chroma_data/chroma.sqlite3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cc3087f1c4cb1a3a703aede6544460ab959e23ff06ade2caf7e0640565572ead
|
| 3 |
+
size 274432
|
db_setup.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from langchain_core.documents import Document
|
| 4 |
+
from langchain_chroma import Chroma
|
| 5 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 6 |
+
|
| 7 |
+
JSON_FILE_PATH = "data/data.json"
|
| 8 |
+
CHROMA_DB_DIR = "chroma_data"
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
if not os.path.exists(JSON_FILE_PATH):
|
| 12 |
+
print(f"Error: File {JSON_FILE_PATH} not found.")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
print("Reading JSON data...")
|
| 16 |
+
with open(JSON_FILE_PATH, "r", encoding="utf-8") as f:
|
| 17 |
+
chili_data = json.load(f)
|
| 18 |
+
|
| 19 |
+
# 3. Konversi ke Format Document LangChain
|
| 20 |
+
print("Converting nested JSON data into LangChain Document format")
|
| 21 |
+
documents = []
|
| 22 |
+
|
| 23 |
+
for label, details in chili_data.items():
|
| 24 |
+
teks_gabungan = f"Penyakit/Kondisi: {label}\n"
|
| 25 |
+
|
| 26 |
+
# Mengecek apakah list ada isinya, lalu digabung dengan newline
|
| 27 |
+
if details.get("penyebab"):
|
| 28 |
+
teks_gabungan += "Penyebab:\n- " + "\n- ".join(details["penyebab"]) + "\n"
|
| 29 |
+
|
| 30 |
+
if details.get("gejala"):
|
| 31 |
+
teks_gabungan += "Gejala:\n- " + "\n- ".join(details["gejala"]) + "\n"
|
| 32 |
+
|
| 33 |
+
if details.get("penanganan"):
|
| 34 |
+
teks_gabungan += "Penanganan:\n- " + "\n- ".join(details["penanganan"]) + "\n"
|
| 35 |
+
|
| 36 |
+
if details.get("pencegahan"):
|
| 37 |
+
teks_gabungan += "Pencegahan:\n- " + "\n- ".join(details["pencegahan"]) + "\n"
|
| 38 |
+
|
| 39 |
+
# Membungkus teks rakitan dan metadata (label yolo)
|
| 40 |
+
doc = Document(
|
| 41 |
+
page_content=teks_gabungan,
|
| 42 |
+
metadata={"label": label}
|
| 43 |
+
)
|
| 44 |
+
documents.append(doc)
|
| 45 |
+
|
| 46 |
+
# 4. Setup Embedding Model
|
| 47 |
+
print("Memuat Embedding Model (all-MiniLM-L6-v2)...")
|
| 48 |
+
embeddings = HuggingFaceEmbeddings(model_name="Qwen/Qwen3-Embedding-0.6B")
|
| 49 |
+
|
| 50 |
+
# 5. Simpan ke ChromaDB via LangChain
|
| 51 |
+
print("Menyimpan data ke ChromaDB...")
|
| 52 |
+
vectorstore = Chroma.from_documents(
|
| 53 |
+
documents=documents,
|
| 54 |
+
embedding=embeddings,
|
| 55 |
+
persist_directory=CHROMA_DB_DIR,
|
| 56 |
+
collection_name="chilicare_kb"
|
| 57 |
+
)
|
| 58 |
+
print(f"Berhasil! Database tersimpan di folder: {CHROMA_DB_DIR}\n")
|
| 59 |
+
|
| 60 |
+
# ==========================================
|
| 61 |
+
# --- TESTING HYBRID RETRIEVAL LANGCHAIN ---
|
| 62 |
+
# ==========================================
|
| 63 |
+
print("--- UJI COBA RETRIEVAL (LANGCHAIN) ---")
|
| 64 |
+
|
| 65 |
+
# Tes A: Jalur YOLO (Metadata Filter)
|
| 66 |
+
test_label = "Bercak" # Menguji label Bercak
|
| 67 |
+
print(f"\n[JALUR YOLO] Mencari data dengan filter label: '{test_label}'")
|
| 68 |
+
|
| 69 |
+
yolo_results = vectorstore.similarity_search(
|
| 70 |
+
query="berikan penjelasan lengkap",
|
| 71 |
+
k=1,
|
| 72 |
+
filter={"label": test_label}
|
| 73 |
+
)
|
| 74 |
+
if yolo_results:
|
| 75 |
+
print("✅ Ditemukan Data RAG (Cuplikan):")
|
| 76 |
+
# Print 200 karakter pertama agar terminal tidak terlalu penuh
|
| 77 |
+
print(f"{yolo_results[0].page_content[:200]}...")
|
| 78 |
+
|
| 79 |
+
# Tes B: Jalur Teks/Chat (Semantic Similarity Search)
|
| 80 |
+
test_query = "daunnya melengkung ke atas seperti mangkuk dan kaku"
|
| 81 |
+
print(f"\n[JALUR TEKS] User bertanya: '{test_query}'")
|
| 82 |
+
|
| 83 |
+
chat_results = vectorstore.similarity_search(query=test_query, k=1)
|
| 84 |
+
if chat_results:
|
| 85 |
+
label_terdeteksi = chat_results[0].metadata['label']
|
| 86 |
+
print(f"✅ Sistem menduga ini penyakit: '{label_terdeteksi}'")
|
| 87 |
+
print("📄 Cuplikan Penjelasan:")
|
| 88 |
+
print(f"{chat_results[0].page_content[:200]}...")
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
main()
|
src/chains/__pycache__/rag.cpython-312.pyc
ADDED
|
Binary file (2.61 kB). View file
|
|
|
src/chains/rag.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain_openai import ChatOpenAI
|
| 3 |
+
from langchain_core.prompts import PromptTemplate
|
| 4 |
+
from langchain_chroma import Chroma
|
| 5 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 6 |
+
|
| 7 |
+
# 1. Inisialisasi LLM (Tetap sama)
|
| 8 |
+
llm = ChatOpenAI(
|
| 9 |
+
base_url="https://openrouter.ai/api/v1",
|
| 10 |
+
api_key=os.getenv("OPENROUTER_API_KEY"),
|
| 11 |
+
model="nvidia/nemotron-3-nano-30b-a3b:free",
|
| 12 |
+
temperature=0.6,
|
| 13 |
+
max_tokens=1500
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# 2. Koneksi ke ChromaDB Lokal
|
| 17 |
+
print("Memuat koneksi ke Database...")
|
| 18 |
+
embeddings = HuggingFaceEmbeddings(model_name="Qwen/Qwen3-Embedding-0.6B")
|
| 19 |
+
vectorstore = Chroma(
|
| 20 |
+
persist_directory="chroma_data", # Pastikan nama foldernya sama dengan di db_setup.py
|
| 21 |
+
embedding_function=embeddings,
|
| 22 |
+
collection_name="chilicare_kb"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# 3. Setup Prompt Template (Lebih Ringkas)
|
| 26 |
+
# Kita hanya butuh 'context' karena isi penyebab/gejala sudah dirakit oleh db_setup.py
|
| 27 |
+
prompt_template = PromptTemplate(
|
| 28 |
+
input_variables=["disease_name", "context"],
|
| 29 |
+
template="""
|
| 30 |
+
Anda adalah penyuluh pertanian cabai.
|
| 31 |
+
|
| 32 |
+
Gunakan HANYA informasi referensi berikut. Jangan menambahkan fakta baru di luar referensi.
|
| 33 |
+
|
| 34 |
+
--- REFERENSI DATABASE ---
|
| 35 |
+
{context}
|
| 36 |
+
--------------------------
|
| 37 |
+
|
| 38 |
+
Tugas:
|
| 39 |
+
Jelaskan tentang kondisi/penyakit "{disease_name}" berdasarkan referensi di atas.
|
| 40 |
+
Gunakan bahasa yang sederhana, ramah, dan mudah dipahami oleh petani cabai.
|
| 41 |
+
Jika kondisinya adalah "Sehat", berikan apresiasi dan jelaskan bahwa tanaman dalam kondisi prima(generate jika label gambar output adalah "sehat").
|
| 42 |
+
"""
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# 4. Buat Chain LCEL
|
| 46 |
+
chain = prompt_template | llm
|
| 47 |
+
|
| 48 |
+
# 5. Fungsi Generate Narrative Terintegrasi RAG
|
| 49 |
+
def generate_narrative(disease_name):
|
| 50 |
+
print(f"Mencari data untuk label: {disease_name}...")
|
| 51 |
+
|
| 52 |
+
# RETRIEVAL: Mengambil data dari ChromaDB menggunakan Metadata Filter (Jalur YOLO)
|
| 53 |
+
# Ini memastikan data yang ditarik 100% akurat sesuai label
|
| 54 |
+
results = vectorstore.similarity_search(
|
| 55 |
+
query="berikan penjelasan lengkap", # Query dummy karena kita mengandalkan filter
|
| 56 |
+
k=1,
|
| 57 |
+
filter={"label": disease_name}
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if not results:
|
| 61 |
+
return f"⚠️ Data penyakit '{disease_name}' tidak ditemukan di database."
|
| 62 |
+
|
| 63 |
+
# Ambil teks referensi utuh dari hasil retrieval
|
| 64 |
+
retrieved_context = results[0].page_content
|
| 65 |
+
|
| 66 |
+
print("Data ditemukan. Menghasilkan narasi dengan LLM...")
|
| 67 |
+
# GENERATION: Kirim konteks dan nama penyakit ke LLM
|
| 68 |
+
response = chain.invoke({
|
| 69 |
+
"disease_name": disease_name,
|
| 70 |
+
"context": retrieved_context
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
return response.content
|
| 74 |
+
|
| 75 |
+
# --- TEST SCRIPT ---
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
# Pastikan OPENROUTER_API_KEY sudah tersetting di environment variables kamu
|
| 78 |
+
# Contoh penggunaan dari hasil output YOLO
|
| 79 |
+
label_yolo = "Keriting"
|
| 80 |
+
hasil = generate_narrative(label_yolo)
|
| 81 |
+
|
| 82 |
+
print("\n=== HASIL GENERATE ===")
|
| 83 |
+
print(hasil)
|