create: add detection file using fastapi for prompting on ollama model

This commit is contained in:
abiyasa05 2025-06-09 10:51:41 +07:00
parent 52e13af4c1
commit b4e0635329
4 changed files with 207 additions and 412 deletions

View File

@ -1,442 +1,177 @@
# from fastapi import FastAPI, HTTPException
# from fastapi.middleware.cors import CORSMiddleware
# import httpx
# import logging
# import traceback
# import random # Untuk memilih cerita secara acak
# app = FastAPI()
# OLLAMA_URL = "http://192.168.60.92:11434/api/generate"
# # Konfigurasi Logging
# logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# # Data ringkasan cerita
# summaries = {
# "Malin Kundang": "Malin Kundang adalah seorang anak dari keluarga miskin yang menjadi kaya raya namun menolak mengakui ibunya, hingga akhirnya dikutuk menjadi batu.",
# "Bawang Merah Bawang Putih": "Bawang Putih adalah gadis baik hati yang diperlakukan buruk oleh ibu dan saudara tirinya, tetapi kebaikannya membuahkan hasil berkat ikan ajaib.",
# "Sangkuriang": "Sangkuriang jatuh cinta pada ibunya, Dayang Sumbi, dan diberi tugas mustahil untuk membangun perahu dalam satu malam. Ia gagal dan akhirnya marah, menendang perahu hingga menjadi Gunung Tangkuban Perahu.",
# "Si Kancil": "Si Kancil dengan kecerdikannya berhasil menipu buaya untuk menyeberangi sungai dengan aman."
# }
# @app.get("/generate/")
# async def generate_text():
# try:
# # Pilih cerita secara acak
# selected_story = random.choice(list(summaries.keys()))
# story_summary = summaries[selected_story]
# # Buat prompt untuk Ollama
# prompt = f"""Buatlah tiga soal literasi dan jawabannya berdasarkan ringkasan cerita berikut:
# Cerita:
# {story_summary}
# Format output:
# Soal 1: [Tulis soal pertama di sini]
# Jawaban: [Tulis jawaban pertama di sini]
# Soal 2: [Tulis soal kedua di sini]
# Jawaban: [Tulis jawaban kedua di sini]
# Soal 3: [Tulis soal ketiga di sini]
# Jawaban: [Tulis jawaban ketiga di sini]
# """
# payload = {
# "model": "llama3.1:latest",
# "prompt": prompt,
# "stream": False
# }
# logging.info(f"Sending request to Ollama: {payload}")
# # Kirim request ke Ollama dengan timeout
# async with httpx.AsyncClient(timeout=30) as client:
# response = await client.post(OLLAMA_URL, json=payload)
# # Log response dari Ollama
# logging.info(f"Response status code: {response.status_code}")
# logging.info(f"Response content: {response.text}")
# # Raise error jika status code bukan 2xx
# response.raise_for_status()
# # Coba parse JSON response
# try:
# result = response.json()
# logging.info(f"Parsed response: {result}")
# return {
# "selected_story": selected_story,
# "generated_questions": result
# }
# except Exception as e:
# logging.error(f"Failed to parse JSON response: {response.text}")
# raise HTTPException(status_code=500, detail="Invalid response format from Ollama API")
# except httpx.HTTPStatusError as e:
# logging.error(f"HTTP error from Ollama API: {e.response.text}")
# raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
# except Exception as e:
# error_trace = traceback.format_exc() # Dapatkan traceback lengkap
# logging.error(f"Unexpected error: {error_trace}") # Cetak error di log
# raise HTTPException(status_code=500, detail="Internal Server Error")
# # Endpoint untuk mengecek koneksi ke Ollama
# @app.get("/test-connection/")
# async def test_connection():
# try:
# async with httpx.AsyncClient(timeout=10) as client:
# response = await client.get("http://192.168.60.92:11434")
# return {"status": response.status_code, "content": response.text}
# except Exception as e:
# logging.error(f"Connection test failed: {str(e)}")
# return {"error": str(e)}
# app = FastAPI()
# OLLAMA_URL = "http://192.168.60.92:11434/api/generate"
# logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# class GenerateRequest(BaseModel):
# content: str # Menerima teks bebas, bukan content_id
# @app.post("/generate/")
# async def generate_text(request: GenerateRequest):
# try:
# if not request.content.strip():
# raise HTTPException(status_code=400, detail="Content cannot be empty")
# prompt = f"""Buatlah tiga soal literasi dan jawabannya berdasarkan teks berikut:
# {request.content}
# Format output:
# Soal 1: [Tulis soal pertama di sini]
# Jawaban: [Tulis jawaban pertama di sini]
# Jawaban: [Tulis jawaban kedua di sini]
# Soal 2: [Tulis soal kedua di sini]
# Jawaban: [Tulis jawaban pertama di sini]
# Jawaban: [Tulis jawaban kedua di sini]
# Soal 3: [Tulis soal ketiga di sini]
# Jawaban: [Tulis jawaban pertama di sini]
# Jawaban: [Tulis jawaban kedua di sini]
# """
# payload = {
# "model": "llama3.1:latest",
# "prompt": prompt,
# "stream": False
# }
# async with httpx.AsyncClient(timeout=30) as client:
# response = await client.post(OLLAMA_URL, json=payload)
# response.raise_for_status()
# result = response.json()
# return result
# except Exception as e:
# logging.error(f"Error: {traceback.format_exc()}")
# raise HTTPException(status_code=500, detail="Internal Server Error")
# from fastapi import FastAPI, HTTPException
# from pydantic import BaseModel
# import httpx
# import logging
# import traceback
# app = FastAPI()
# OLLAMA_URL = "http://192.168.60.92:11434/api/generate"
# class GenerateRequest(BaseModel):
# content: str = "Buatlah soal literasi berdasarkan teks anak-anak tentang lingkungan."
# @app.post("/generate/")
# async def generate_text(request: GenerateRequest):
# # Prompt default jika kosong
# prompt = f"""Buatlah tiga soal literasi berdasarkan teks berikut:
# {request.content}
# Format output:
# Soal 1: [Tulis soal pertama di sini]
# Jawaban: [Tulis jawaban pertama di sini]
# Soal 2: [Tulis soal kedua di sini]
# Jawaban: [Tulis jawaban kedua di sini]
# Soal 3: [Tulis soal ketiga di sini]
# Jawaban: [Tulis jawaban ketiga di sini]
# """
# payload = {
# "model": "llama3.1:latest",
# "prompt": prompt,
# "stream": False
# }
# async with httpx.AsyncClient() as client:
# response = await client.post(OLLAMA_URL, json=payload)
# response.raise_for_status()
# return response.json()
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import httpx
import logging
import random
from pydantic import BaseModel
import os, logging, httpx, hashlib
from utils_file import read_text_from_file
# Logging setup
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Ubah sesuai kebutuhan jika ada pembatasan domain
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
OLLAMA_URL = "http://localhost:11434/api/generate"
# Path lokal file materi
MATERIAL_BASE_PATH = r"D:\Projek Skripsi\Penilaian Literasi\iClOP-V2\storage\app\public"
OLLAMA_URL = "http://labai.polinema.ac.id:11434/api/generate"
# Konfigurasi Logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# Request model
class FileMaterialRequest(BaseModel):
file_name: str
question_type: str
question_count: int
start_page: int = 10
# Data ringkasan cerita, pantun, dan puisi (bisa ditambah lagi)
data_sources = {
"cerita": {
"Malin Kundang": "Malin Kundang adalah seorang anak dari keluarga miskin yang menjadi kaya raya namun menolak mengakui ibunya, hingga akhirnya dikutuk menjadi batu.",
"Bawang Merah Bawang Putih": "Bawang Putih adalah gadis baik hati yang diperlakukan buruk oleh ibu dan saudara tirinya, tetapi kebaikannya membuahkan hasil berkat ikan ajaib.",
"Sangkuriang": "Sangkuriang jatuh cinta pada ibunya, Dayang Sumbi, dan diberi tugas mustahil untuk membangun perahu dalam satu malam. Ia gagal dan akhirnya marah, menendang perahu hingga menjadi Gunung Tangkuban Perahu.",
"Si Kancil": "Si Kancil dengan kecerdikannya berhasil menipu buaya untuk menyeberangi sungai dengan aman.",
"Timun Mas": "Seorang ibu tua mendapatkan anak dari biji timun emas. Namun, anak itu harus melarikan diri dari raksasa jahat yang ingin memakannya."
},
"pantun": {
"Pantun Nasihat": "Jalan-jalan ke kota Blitar,\nJangan lupa membeli roti.\nRajin belajar sejak pintar,\nAgar sukses di kemudian hari.",
"Pantun Jenaka": "Ke pasar beli ikan teri,\nIkan habis tinggal kepala.\nJangan suka mencuri,\nNanti ketahuan malah celaka."
},
"puisi": {
"Puisi Alam": "Langit biru membentang luas,\nBurung-burung terbang bebas.\nAngin sepoi menyapu dedaunan,\nAlam indah penuh kedamaian.",
"Puisi Persahabatan": "Sahabat sejati selalu ada,\nDalam suka dan dalam duka.\nBersama kita jalani hari,\nMengukir cerita tak terlupa."
}
}
class FeedbackRequest(BaseModel):
user_answer: str
expected_answer: str
@app.post("/generate/")
async def generate_text():
feedback_cache = {}
def potong_konten(text: str, max_chars: int = 5000):
return text[:max_chars] if len(text) > max_chars else text
@app.post("/generate-from-file/")
async def generate_from_file(request: FileMaterialRequest):
try:
# Pilih 3 cerita, 1 pantun, dan 1 puisi secara acak
selected_stories = random.sample(list(data_sources["cerita"].keys()), 3)
selected_pantun = random.choice(list(data_sources["pantun"].keys()))
selected_puisi = random.choice(list(data_sources["puisi"].keys()))
if request.question_count < 1 or request.question_count > 20:
raise HTTPException(status_code=400, detail="Jumlah soal harus antara 120")
# Buat format prompt dengan cerita, pantun, dan puisi
story_prompts = "\n\n".join([
f"**{story}**\n\n{data_sources['cerita'][story]}\n\nBerdasarkan cerita ini, buatlah **3 soal literasi** dalam format pilihan ganda."
for story in selected_stories
])
if request.question_type not in ["multiple_choice", "essay"]:
raise HTTPException(status_code=400, detail="Jenis soal tidak valid. Pilih: multiple_choice atau essay")
pantun_prompt = f"**{selected_pantun}**\n\n{data_sources['pantun'][selected_pantun]}\n\nBerdasarkan pantun ini, buatlah **1 soal literasi** dalam format pilihan ganda."
mc_count = request.question_count if request.question_type == "multiple_choice" else 0
essay_count = request.question_count if request.question_type == "essay" else 0
puisi_prompt = f"**{selected_puisi}**\n\n{data_sources['puisi'][selected_puisi]}\n\nBerdasarkan puisi ini, buatlah **1 soal literasi** dalam format pilihan ganda."
file_path = os.path.join(MATERIAL_BASE_PATH, request.file_name)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"File tidak ditemukan: {file_path}")
# Gabungkan semua prompt
full_prompt = f"""
Buatlah soal berdasarkan teks berikut ini:
# Baca isi file mulai dari halaman tertentu
text = read_text_from_file(file_path, start_page=request.start_page)
if not text.strip():
raise HTTPException(status_code=400, detail="Isi file kosong atau tidak terbaca.")
{story_prompts}
content_bersih = potong_konten(text.strip())
{pantun_prompt}
prompt = f"""
Buat soal latihan berdasarkan teks materi berikut untuk siswa SD kelas 3.
{puisi_prompt}
**Instruksi:**
1. Buat total {request.question_count} soal dengan rincian:
- Soal pilihan ganda: {mc_count}
- Soal isian: {essay_count}
Pastikan soal yang dibuat beragam, berbobot untuk siswa SD, dan tidak hanya berasal dari satu jenis teks.
"""
2. **Untuk soal isian:**
- Ambil kutipan **minimal dua kalimat yang saling terhubung** dari teks sebagai dasar soal
- Awali dengan: **Bacalah kutipan berikut: "[kutipan]"**
- Buat pertanyaan berdasarkan kutipan tersebut
- Sertakan **jawaban singkat**
- Beri bobot antara **35** sesuai kompleksitas
payload = {
3. **Untuk soal pilihan ganda (jika ada):**
- Ambil kutipan **1 kalimat yang relevan** dari teks
- Buat pertanyaan dan 4 pilihan jawaban (AD)
- Beri jawaban benar dan bobot antara **12** sesuai tingkat kesulitan
4. Gunakan bahasa sederhana dan sesuai dengan siswa SD kelas 3.
5. Jangan menambahkan informasi di luar teks materi.
**Format Output:**
""" + ("""
**Soal Pilihan Ganda:**
1. Bacalah kutipan berikut: "[2 kalimat atau lebih dari teks]"
Pertanyaan: [Pertanyaan]
A. [Opsi A]
B. [Opsi B]
C. [Opsi C]
D. [Opsi D]
Jawaban: [Huruf Opsi]
Bobot: [1 atau 2]
""" if mc_count > 0 else "") + ("""
**Soal Isian:**
1. Bacalah kutipan berikut: "[2 kalimat atau lebih dari teks]". [Pertanyaan]
Jawaban: [Jawaban]
Bobot: [3 - 5]
""" if essay_count > 0 else "") + f"""
---
**Materi:**
{content_bersih}
""".strip()
logging.info("Mengirim prompt ke Ollama...")
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(OLLAMA_URL, json={
"model": "llama3.1:latest",
"prompt": full_prompt,
"stream": False
"prompt": prompt,
"stream": False,
"options": {
"num_predict": 2048
}
})
logging.info(f"Mengirim permintaan ke Ollama: {payload}")
# Kirim request ke Ollama
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(OLLAMA_URL, json=payload)
# Log response dari Ollama
logging.info(f"Response status code: {response.status_code}")
logging.info(f"Response content: {response.text}")
# Raise error jika status code bukan 2xx
response.raise_for_status()
# Ambil hasil respon dan parsing JSON
result = response.json()
# Pastikan hasil tidak kosong
generated_text = result.get("response", "").strip()
if not generated_text:
raise HTTPException(status_code=500, detail="Ollama tidak menghasilkan pertanyaan")
return {
"selected_stories": selected_stories,
"selected_pantun": selected_pantun,
"selected_puisi": selected_puisi,
"generated_questions": generated_text
"generated_questions": generated_text,
"question_type": request.question_type,
"question_count": request.question_count,
"mc_count": mc_count,
"essay_count": essay_count,
"start_page": request.start_page
}
except httpx.HTTPStatusError as e:
logging.error(f"HTTP error dari Ollama API: {e.response.text}")
raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
except Exception as e:
logging.error(f"Error saat generate dari file: {e}")
raise HTTPException(status_code=500, detail=f"Terjadi kesalahan internal: {str(e)}")
@app.post("/generate-feedback/")
async def generate_feedback(request: FeedbackRequest):
try:
user_answer = request.user_answer.strip()
expected_answer = request.expected_answer.strip()
prompt_hash = hashlib.sha256(f"{user_answer}|{expected_answer}".encode()).hexdigest()
if prompt_hash in feedback_cache:
logging.info("Feedback dari cache.")
return {"feedback": feedback_cache[prompt_hash]}
prompt = f"""
Kamu adalah asisten pengajar untuk siswa SD kelas 3. Siswa memberikan jawaban berikut untuk soal isian.
**Jawaban Siswa:** {user_answer}
**Jawaban Ideal:** {expected_answer}
Beri feedback singkat dan membangun, maksimal 2 kalimat. Gunakan bahasa yang mudah dimengerti oleh siswa SD. Jika jawaban siswa salah, berikan petunjuk atau koreksi yang membantu.
"""
logging.info("Mengirim prompt feedback ke Ollama...")
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(OLLAMA_URL, json={
"model": "llama3.1:latest",
"prompt": prompt,
"stream": False
})
response.raise_for_status()
result = response.json()
feedback = result.get("response", "").strip()
feedback_cache[prompt_hash] = feedback
return {"feedback": feedback}
except Exception as e:
logging.error(f"Terjadi kesalahan: {str(e)}")
raise HTTPException(status_code=500, detail="Terjadi kesalahan internal")
# from fastapi import FastAPI, HTTPException
# from fastapi.middleware.cors import CORSMiddleware
# import httpx
# import logging
# import random
# import re
# import traceback
# app = FastAPI()
# app.add_middleware(
# CORSMiddleware,
# allow_origins=["*"],
# allow_credentials=True,
# allow_methods=["*"],
# allow_headers=["*"],
# )
# OLLAMA_URL = "http://192.168.60.92:11434/api/generate"
# # Konfigurasi Logging
# logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# # Data ringkasan cerita, pantun, dan puisi
# data_sources = {
# "cerita": {
# "Malin Kundang": "Malin Kundang adalah seorang anak dari keluarga miskin yang menjadi kaya raya namun menolak mengakui ibunya, hingga akhirnya dikutuk menjadi batu.",
# "Bawang Merah Bawang Putih": "Bawang Putih adalah gadis baik hati yang diperlakukan buruk oleh ibu dan saudara tirinya, tetapi kebaikannya membuahkan hasil berkat ikan ajaib.",
# "Sangkuriang": "Sangkuriang jatuh cinta pada ibunya, Dayang Sumbi, dan diberi tugas mustahil untuk membangun perahu dalam satu malam. Ia gagal dan akhirnya marah, menendang perahu hingga menjadi Gunung Tangkuban Perahu.",
# "Si Kancil": "Si Kancil dengan kecerdikannya berhasil menipu buaya untuk menyeberangi sungai dengan aman.",
# "Timun Mas": "Seorang ibu tua mendapatkan anak dari biji timun emas. Namun, anak itu harus melarikan diri dari raksasa jahat yang ingin memakannya."
# },
# "pantun": {
# "Pantun Nasihat": "Jalan-jalan ke kota Blitar,\nJangan lupa membeli roti.\nRajin belajar sejak pintar,\nAgar sukses di kemudian hari.",
# "Pantun Jenaka": "Ke pasar beli ikan teri,\nIkan habis tinggal kepala.\nJangan suka mencuri,\nNanti ketahuan malah celaka."
# },
# "puisi": {
# "Puisi Alam": "Langit biru membentang luas,\nBurung-burung terbang bebas.\nAngin sepoi menyapu dedaunan,\nAlam indah penuh kedamaian.",
# "Puisi Persahabatan": "Sahabat sejati selalu ada,\nDalam suka dan dalam duka.\nBersama kita jalani hari,\nMengukir cerita tak terlupa."
# }
# }
# @app.post("/generate/")
# async def generate_text():
# try:
# # Pilih 3 cerita, 1 pantun, dan 1 puisi secara acak
# selected_stories = random.sample(list(data_sources["cerita"].keys()), 3)
# selected_pantun = random.choice(list(data_sources["pantun"].keys()))
# selected_puisi = random.choice(list(data_sources["puisi"].keys()))
# # Buat format prompt dengan cerita, pantun, dan puisi
# story_prompts = "\n\n".join([
# f"**{story}**\n\n{data_sources['cerita'][story]}\n\nBerdasarkan cerita ini, buatlah **3 soal literasi** dalam format pilihan ganda."
# for story in selected_stories
# ])
# pantun_prompt = f"**{selected_pantun}**\n\n{data_sources['pantun'][selected_pantun]}\n\nBerdasarkan pantun ini, buatlah **1 soal literasi** dalam format pilihan ganda."
# puisi_prompt = f"**{selected_puisi}**\n\n{data_sources['puisi'][selected_puisi]}\n\nBerdasarkan puisi ini, buatlah **1 soal literasi** dalam format pilihan ganda."
# # Gabungkan semua prompt
# full_prompt = f"""
# Buatlah soal berdasarkan teks berikut ini:
# {story_prompts}
# {pantun_prompt}
# {puisi_prompt}
# Format setiap soal:
# ---
# **Pertanyaan**
# A. Pilihan 1
# B. Pilihan 2
# C. Pilihan 3
# D. Pilihan 4
# Jawaban: (A/B/C/D)
# ---
# """
# payload = {
# "model": "llama3.1:latest",
# "prompt": full_prompt,
# "stream": False
# }
# logging.info(f"Mengirim permintaan ke Ollama: {payload}")
# # Kirim request ke Ollama
# async with httpx.AsyncClient(timeout=60) as client:
# response = await client.post(OLLAMA_URL, json=payload)
# logging.info(f"Response status code: {response.status_code}")
# logging.info(f"Response content: {response.text}")
# response.raise_for_status()
# result = response.json()
# generated_text = result.get("response", "").strip()
# if not generated_text:
# raise HTTPException(status_code=500, detail="Ollama tidak menghasilkan pertanyaan")
# # Parsing hasil teks menjadi daftar soal
# questions = []
# raw_questions = re.split(r'\n\s*\n', generated_text)
# for raw in raw_questions:
# lines = raw.strip().split("\n")
# if len(lines) >= 6:
# question_text = lines[0].strip()
# options = [f"({opt[0]}) {opt[3:].strip()}" for opt in lines[1:5] if len(opt) > 3]
# # Ambil jawaban dengan regex
# answer_match = re.search(r'Jawaban:\s*\(?([A-D])\)?', raw)
# correct_answer = f"({answer_match.group(1)})" if answer_match else "Tidak ditemukan"
# questions.append({
# "question": question_text,
# "options": options,
# "correct_answer": correct_answer
# })
# if not questions:
# raise HTTPException(status_code=500, detail="Parsing soal gagal, format tidak sesuai.")
# return {
# "selected_stories": selected_stories,
# "selected_pantun": selected_pantun,
# "selected_puisi": selected_puisi,
# "generated_questions": questions
# }
# except httpx.HTTPStatusError as e:
# logging.error(f"HTTP error dari Ollama API: {e.response.text}")
# raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
# except Exception as e:
# logging.error(f"Terjadi kesalahan: {str(e)}")
# logging.error(traceback.format_exc()) # Cetak stack trace lengkap
# raise HTTPException(status_code=500, detail="Terjadi kesalahan internal")
logging.error(f"Error saat generate feedback: {e}")
raise HTTPException(status_code=500, detail=f"Terjadi kesalahan: {str(e)}")

View File

@ -0,0 +1,60 @@
import os
from docx import Document
import fitz
def read_text_from_file(filepath: str, start_page: int = 0, max_chars: int = 3000) -> str:
ext = os.path.splitext(filepath)[1].lower()
try:
if ext == '.pdf':
doc = fitz.open(filepath)
if start_page >= len(doc):
return ""
text = ""
for page_num in range(start_page, len(doc)):
text += doc[page_num].get_text()
if len(text) >= max_chars:
break
return text[:max_chars]
elif ext == '.docx':
doc = Document(filepath)
paragraphs = [para.text.strip() for para in doc.paragraphs if para.text.strip()]
if not paragraphs:
return ""
para_per_page = 20
if start_page > 0:
start_index = start_page * para_per_page
if start_index >= len(paragraphs):
return ""
selected_paragraphs = paragraphs[start_index:]
else:
selected_paragraphs = paragraphs
combined_text = "\n".join(selected_paragraphs)
return combined_text[:max_chars]
elif ext == '.txt':
with open(filepath, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not lines:
return ""
lines_per_page = 40
if start_page > 0:
start_index = start_page * lines_per_page
if start_index >= len(lines):
return ""
selected_lines = lines[start_index:]
else:
selected_lines = lines
combined_text = "".join(selected_lines)
return combined_text[:max_chars]
else:
raise Exception(f"Format file tidak didukung: {ext}")
except Exception as e:
raise Exception(f"Gagal membaca file {ext.upper()}: {e}")