frendyrachman's picture
Update app.py
5fce78b verified
import os
from pdf2image import convert_from_path
from datetime import date
from PIL import Image
import gradio as gr
from google import genai
import zipfile
import tempfile
os.system("apt-get install poppler-utils")
import datetime
from docx import Document
import time
import random
from google.genai.types import GenerateContentConfig
import json
from PIL import ImageEnhance, ImageFilter
def extract_zip_and_collect_files(zip_file_path):
"""
Extract zip file to a temp directory and return list of pdf/image file paths inside.
"""
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
# Collect all pdf and image files in extracted folder recursively
collected_files = []
for root, _, files in os.walk(temp_dir):
for f in files:
if f.lower().endswith(('.pdf', '.jpg', '.jpeg', '.png')):
collected_files.append(os.path.join(root, f))
return collected_files
# Function to process a list of PDF files and convert them to images
def process_pdfs(pdf_files, dpi):
"""
Process a list of PDF files, convert each to images, and return all images.
"""
all_images = []
for pdf_file in pdf_files:
if not os.path.isfile(pdf_file):
raise ValueError(f"File {pdf_file} does not exist.")
images = convert_from_path(pdf_file, dpi=dpi)
all_images.extend(images)
return all_images
def preprocess_image(img):
# Enhance contrast and sharpness
img = ImageEnhance.Contrast(img).enhance(1.75)
img = ImageEnhance.Sharpness(img).enhance(2.25)
# Resize for optimal resolution (maintain aspect ratio)
max_size = (1600, 1600)
img.thumbnail(max_size, Image.LANCZOS)
return img
# Function to analyze the extracted image using Google GenAI
def gemini_analysis(images, tanggal_berangkat, tanggal_pulang, tanggal_biometrik):
client = genai.Client(api_key='AIzaSyBpviFHkaEF-GAjMMl28dIS1poikhBqq_w')
# Define your prompt
prompt = '''Anda adalah petugas pemeriksaan kelengkapan dan kesesuaian dokumen untuk pengajuan Visa. Berikut ini adalah alur kerja anda:
1. Ekstrak metadata dari setiap file/gambar yang diberikan berdasarkan data yang dibutuhkan masing-masing dokumen.
2. Dari setiap data yang anda dapat, bandingkan dengan persyaratan per dokumen dan periksa konsistensi data kunci antar dokumen.
3. Berikan analisis per dokumen termasuk kelengkapan dan konsistensi data antar dokumen.
4. Output pemrosesan berupa JSON dengan template di bawah.
5. Data boleh berupa fotokopi (bukan gambar asli).
---
List Dokumen wajib diserahkan seluruh peserta:
KTP:
Wajib ada untuk setiap peserta. Nama harus sama dengan paspor rekening koran, surat referensi bank, bukti kelahiran dan tertera di Kartu Keluarga. Jika ada perbedaan nama, wajib ada surat keterangan beda nama.
Paspor
Wajib ada untuk setiap peserta. Nama harus sama dengan KTP, rekening koran, surat referensi bank, bukti kelahiran dan tertera di kartu keluarga. Jika ada perbedaan nama, wajib ada surat keterangan beda nama.
Menampilkan nama, nomor paspor dan detail identitas lainnya, serta masa aktif minimal 6 bulan setelah tanggal pulang, dan sudah ada tanda tangan.
Kartu Keluarga (KK)
Wajib ada untuk setiap peserta. Nama harus sama dengan KTP, paspor, bukti kelahiran & dokumen lain. Jika ada perbedaan nama, wajib ada surat keterangan beda nama.
Kartu keluarga harus sudah terdapat barcode, dan terbit minimal tahun 2019.
REKENING KORAN PESERTA
Wajib ada untuk setiap peserta. Nama nasabah (pemilik) harus sama dengan KTP, paspor & dokumen lain. Jika ada perbedaan nama, wajib ada surat keterangan beda nama.
Sudah tertera cap dan logo bank, nama pserta dan nomor rekening, saldo minimal Rp 40 juta/orang.
Berikan peringatan di bagian analisis jika ada transaksi mencurigakan dengan nominal yang tidak wajar.
Wajib terupdate hingga 7 hari sebelum tanggal biometrik.
SURAT REFERENSI BANK
Wajib ada untuk setiap peserta. Nama nasabah (pemilik) harus sama dengan KTP, paspor dan dokumen lain.
Harus berupa surat resmi dari bank yang menjelaskan bahwa nama peserta tersebut adalah nasabah bank tersebut. Bukan rekening koran dan tidak bisa digantikan dengan rekening koran
PAS FOTO
Background putih, wajah terlihat 80%, alis terlihat, tidak berbayang. Pastikan anda tidak mendeteksi tempat meletakan foto sebagai Background.
Pasfoto bisa saja difoto dengan diletakkan di atas meja atau objek berwarnai lainnya.
BUKTI KELAHIRAN
Wajib ada untuk setiap peserta. Nama yang tertulis harus sama dengan KTP, PASPOR dan terdapat di Kartu Keluarga (KK).
Dapat digantikan dengan ijazah. Nama yang tertulis harus sama dengan KTP, PASPOR dan terdapat di Kartu Keluarga (KK).
---
Dokumen lain yang sifatnya kondisional
BUKTI NIKAH/CERAI
Jika status perkawinan di KTP tertulis "Kawin" maka wajib ada Bukti Nikah (misal buku nikah/akta nikah).
Jika status perkawinan di KTP tertulis "Cerai" maka wajib ada bukti cerai yang menyatakan cerai mati atau cerai hidup.
Jika status perkawinan di KTP tertulis "Belum Kawin" maka tidak perlu.
GUARANTEE LETTER
Merupakan surat yang berisi jaminan terkait perjalanan peserta antara lain tujuan perjalanan, tanggal berangkat, tanggal pulang, penanggung biaya, dan jaminan akan kembali ke Indonesia.
Guarantee Letter bisa berasal dari institusi pendidikan seperti sekolah/universitas, tempat peserta bekerja atau dari keluarga seperti suami atau orang tua.
Surat harus mencantumkan nama peserta yang sama dengan KTP, dalam bahasa Inggris, berisi tujuan, tanggal trip, siapa penanggung biaya, dan jaminan kembali ke Indonesia.
SURAT SPONSOR
Berbeda dengan Guarantee Letter. Merupakan surat yang menyatakan bahwa peserta disponsori oleh pihak ketiga khususnya sebagai penanggung biaya. Laporkan data pemberi sponsor.
Bisa berasal dari perusahaan tempat peserta bekerja atau sekolah/universitas. Jika surat sponsor dari sekolah/universitas,maka wajib ada KARTU PELAJAR/MAHASISWA
Surat harus mencantumkan nama peserta yang sama dengan KTP, dalam bahasa Inggris, berisi tujuan, tanggal trip, identitas penanggung biaya, dan jaminan kembali ke Indonesia.
SLIP GAJI
Jika peserta menyertakan SURAT SPONSOR / GUARANTEE LETTER dari perusahaan tempat peserta bekerja, peserta WAJIB menyertakan SLIP GAJI 3 BULAN terakhir sebelum tanggal berangkat.
Berupa pembayaran GAJI dari tempat kerja ke peserta. Pastikan NAMA PERUSAHAAN PEMBERI GAJI sama dengan Guarantee Letter dan/atau Surat Sponsor.
NIB/SIUP
Jika peserta merupakan pemilik usaha, wajib tertera nama pemilik usaha, nama usaha, alamat usaha, dan nomor NIB. Nama pemilik usaha harus sama dengan KTP, paspor dan dokumen lain.
KONTRAK FREELANCE
Jika peserta merupakan freelancer, maka wajib mencantumkan surat yang menunjukan bukti kontrak freelance. Harus tertera nama peserta yang sama dengan KTP dan dilengkapi bukti transaksi.
BUKTI TRANSAKSI
Jika peserta merupakan pekerja freelance, selain wajib menyertakan bukti KONTRAK FREELANCE, wajib menyertakan BUKTI TRANSAKSI. Bisa berupa invoice project atau dokumen terkait.
SURAT PENSIUN
Merupakan surat yang menyatakan sudah pensiun. Jika peserta menyertakan surat pensiun, wajib ada Guarantee Letter dari Keluarga.
maka wajib menyertakan surat pensiun yang berisi nama peserta, dan menyatakan sudah pensiun.
REKENING KORAN SPONSOR
Hanya wajib jika peserta disponsori dengan menyertakan SURAT SPONSOR.
Pastikan nama di rekening koran sponsor sama dengan pemberi sponsor yang tertulis di surat sponsor sebagai pemberi sponsor.
Wajib sudah tertera cap & logo bank, nama & nomor rekening, saldo minimal Rp 40 juta, wajib terupdate hingga. 7 hari sebelum tanggal
SURAT REFERENSI BANK SPONSOR
Hanya wajib jika peserta disponsori dengan menyertakan SURAT SPONSOR dan rekening koran dari pemberi sponsor.
Surat Bank Reference/Referensi Bank, merupakan surat yang menyatakan orang tersebut selaku pemberi sponsor merupakan nasabah bank terkait.
---
TEMPLATE OUTPUT JAWABAN DALAM FORMAT JSON:
{
"analysis":
{
"nama_dokumen_1": {"status": "VALID / INVALID / Tidak Perlu",
"description": "penjelasan detail",
"nama_dokumen_2": {"status": "VALID / INVALID / Tidak Perlu",
"description": "penjelasan detail",
"nama_dokumen_1": {"status": "VALID / INVALID / Tidak Perlu",
"description": "penjelasan detail",
},
"summary": "...",
"invalid_item": ["nama_dokumen_1", "nama_dokumen_2"],
"notice_msg": "...",
"form_filling": {"Surname":"...",
"First Name":"...",
"Date of Birth":"...",
"Place of Birth":"...",
"Nationality":"...",
"Sex":"...",
"Mariage Status":"...",
"Passport Number":"...",
"Passport Expiry Date":"...",
"National Identity Number":"...",
"Travel Document Type":"...",
"Travel Document Number":"...",
"Date Of Issue":"...",
"Valid Until":"...",
"Issued Country":"...",
"Applicant's Home Address":"...",
"Applicant's Telephone Number":"...",
"Applicant's Email Address":"...",
"Current Occupation":"...",
"Employer/Educational Address":"...",
"Journey Purpose":"...",
"Destination":"...",
"Duration":"...",
"Number of Entries":"...", # single or multiple
"already has fingerprint":"...",
"inviting person from each destination":"...",
"inviting person email address":"...",
"Traveling and living cost covered by":"..."
} # Hanya isi yang ada di dokumen saja. Jika tidak ada, tidak usah diisi atau dimunculkan. Anda juga bisa menambahkan data penting lain yang mungkin belum ditulis di atas.
}
---
TEMPLATE PESAN PEMBERITAHUAN (notice_msg):
Berikut kami informasikan kekurangan dokumen yang *WAJIB* dibawa saat biometric visa schengen nanti ya :
1. Pas Foto (Wajib berukuran 3,5 x 4,5 cm dan diserahkan di lokasi biometrik)
2. ...
3. ...
4. ...
5. ...
---
## cantumkan dokumen lain yang masih Invalid beserta perbaikan yang diperlukan.
---
TEMPLATE SUMMARY:
List Dokumen yang sudah valid: .... \n
List Dokumen yang invalid : ... \n
Analisis detail keseluruhan: ... \n # sebutkan detail analisa masing-masing dokumen, apa yang menyebabkan dokumen-dokumen tersebut invalid
Rangkuman data peserta: ... # jelaskan rangkuman data peserta yang ada di dokumen seperti nama peserta, apakah disponsori, siapa yang mensponsori, tujuan, tanggal berangkat dan pulang dan data penting lainnya.
'''
# Perform document analysis
prompt_with_date = f'Tanggal Berangkat={tanggal_berangkat}. Tanggal pulang={tanggal_pulang}. Tanggal Biometrik={tanggal_biometrik}\n\n{prompt}'
response = client.models.generate_content(
model="gemini-2.0-flash-lite",
contents=[prompt_with_date] + images,
config=GenerateContentConfig(
temperature=0.1,
response_mime_type="application/json"
)
)
pre_token_usage = response.usage_metadata.total_token_count
token_usage = pre_token_usage*5/1000
raw_output = response.text
# βœ… Inisialisasi variabel default
analysis = {}
summary = ""
invalid_list = []
notice_msg = ""
form_filling = {}
try:
parsed_output = json.loads(response.text)
analysis = parsed_output.get("analysis", {})
analysis_str = json.dumps(analysis, indent=2, ensure_ascii=False)
summary = parsed_output.get("summary", "")
invalid_list = parsed_output.get("invalid_item", [])
invalid_list_str = json.dumps(invalid_list, indent=2, ensure_ascii=False)
notice_msg = parsed_output.get("notice_msg", "")
notice_msg_ending = '''
Note :
β–ͺ️ Semua dokumen di atas mohon difotokan terlebih dahulu agar dapat diperiksa kembali yaa kak πŸ™πŸ»
β–ͺ️ Selama visa belum keluar, rekening harus tetap stabil dengan saldo minimal 40juta rupiah/ orang. Tidak berlaku rekening deposito. Rekening koran wajib tertera nama, nomor rekening, nama dan logo/ stamp bank.
Demikian yang kami sampaikan. Terima kasih atas kerja samanya πŸ€—
Wishtravelers WT
PT WISATA IMPIAN UNIVERSAL
'''
notice_msg = '''🌻 *(WISH TRAVELERS) UPDATE DOKUMEN PENGAJUAN VISA* 🌻
Penting, mohon di baca sampai habis ya kak πŸ™πŸ»
Ini adalah nomor sistem, tidak dapat membalas pesan. Apabila ada pertanyaan, silahkan hubungi mimin WT wa.me/6282123038484
Halo Kak selamat malam 😊
''' + notice_msg + notice_msg_ending
form_filling = parsed_output.get("form_filling", "")
form_filling_str = json.dumps(form_filling, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error parsing JSON: {e}")
return raw_output, analysis_str, summary, invalid_list_str, notice_msg, form_filling_str, token_usage
def process_and_zip_all_images(images, zip_name="All_PDF_Docs.zip"):
# Inisialisasi Gemini client
client = genai.Client(api_key='AIzaSyBpviFHkaEF-GAjMMl28dIS1poikhBqq_w')
# Prompt untuk klasifikasi nama file
prompt_3 = '''Anda adalah asisten yang membantu menamai file gambar dokumen.
Tugas Anda adalah mengidentifikasi jenis dokumen pada gambar ini dan memberikan nama file yang sesuai.
Jawaban Anda *harus* berupa *salah satu* nama file dari daftar berikut:
['paspor', 'fotokopipaspor', 'pasfoto', 'kartukeluarga', 'buktinikah', 'bukticerai', 'ktp', 'buktikelahiran', 'suratsponsor', 'guaranteeletter', 'suratkerja', 'NIB' 'SIUP', 'suratjaminanstaff', 'suratsekolah', 'kontrakkerja', 'suratpensiun', 'rekeningkoran', 'slipgaji', 'suratreferensibank']
Jawaban Anda *hanya* boleh berupa teks yang *persis sama* dengan salah satu item dalam daftar tersebut.
Jangan tambahkan penjelasan, tanda kutip, titik, atau teks tambahan lainnya.
Contoh:
Gambar : [tampak gambar KTP]
Output: KTP
Gambar: [gambar sebenarnya]
Output:
'''
# Step 1: Klasifikasi & Penamaan
renamed_images = []
for image in images:
response = client.models.generate_content(
model="gemini-2.0-flash-lite",
contents=[prompt_3, image],
config=GenerateContentConfig(
temperature=0.1,
top_p=0.1
)
)
filename = response.text.strip().lower()
renamed_images.append({"image": image, "filename": filename})
# Step 2: Kelompokkan berdasarkan nama file (tanpa ekstensi)
grouped = {}
for item in renamed_images:
name = os.path.splitext(item["filename"])[0]
grouped.setdefault(name, []).append(item["image"])
# Step 3: Simpan ke PDF dan masukkan ke ZIP
temp_dir = tempfile.mkdtemp()
zip_path = os.path.join(tempfile.gettempdir(), zip_name)
with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
for doc_name, images in grouped.items():
images_rgb = [img.convert("RGB") for img in images]
pdf_path = os.path.join(temp_dir, f"{doc_name}.pdf")
if len(images_rgb) == 1:
images_rgb[0].save(pdf_path, save_all=True)
else:
images_rgb[0].save(pdf_path, save_all=True, append_images=images_rgb[1:])
zipf.write(pdf_path, arcname=f"{doc_name}.pdf")
return zip_path
def main_process(files, tanggal_berangkat, tanggal_pulang, tanggal_biometrik, dpi):
all_images = []
image_paths_for_zip = []
for file in files:
file_path = file.name if hasattr(file, 'name') else file
if file_path.lower().endswith('.zip'):
extracted_files = extract_zip_and_collect_files(file_path)
for extracted_file in extracted_files:
if extracted_file.lower().endswith('.pdf'):
images = process_pdfs([extracted_file], dpi)
all_images.extend(images)
elif extracted_file.lower().endswith(('.jpg', '.jpeg', '.png')):
image = Image.open(extracted_file)
all_images.append(image)
elif file_path.lower().endswith('.pdf'):
images = process_pdfs([file_path], dpi)
all_images.extend(images)
elif file_path.lower().endswith(('.jpg', '.jpeg', '.png')):
image = Image.open(file_path)
all_images.append(image)
else:
raise ValueError(f"File {file_path} is not a valid image, PDF, or ZIP.")
# Generate summary from images
preprocessed_images = [preprocess_image(img) for img in all_images]
raw_output, analysis_str, summary, invalid_list_str, notice_msg, form_filling_str, token_usage = gemini_analysis(preprocessed_images, tanggal_berangkat, tanggal_pulang, tanggal_biometrik)
rdf = random.randint(5, 10)
time.sleep(rdf)
# Create DOCX for summary output
doc = Document()
doc.add_heading("Visa Document Check Summary", level=1)
doc.add_paragraph(f"Tanggal Berangkat: {tanggal_berangkat}")
doc.add_paragraph(f"Tanggal Pulang: {tanggal_pulang}")
for line in analysis_str.split("\n"):
doc.add_paragraph(line)
doc.add_paragraph(f"Summary: {summary}\n\n")
doc.add_paragraph(f"Invalid List: {invalid_list_str}\n\n")
doc.add_paragraph(f"Notice Message: {notice_msg}\n\n")
doc.add_paragraph(f"Form Filling: {form_filling_str}\n\n")
first_file = files[0]
first_filename = os.path.basename(first_file.name if hasattr(first_file, 'name') else first_file)
base_name = os.path.splitext(first_filename)[0]
docx_filename = f"summary_{base_name}.docx"
temp_docx_path = os.path.join(tempfile.gettempdir(), docx_filename)
doc.save(temp_docx_path)
# Filtering the file
# zip_file_path = process_and_zip_all_images(all_images, zip_name=f'All_PDF_Docs_{base_name}.zip')
return temp_docx_path, form_filling_str, invalid_list_str, analysis_str, summary, notice_msg, token_usage
# Gradio UI update: add ".zip" to accepted file types
with gr.Blocks() as demo:
gr.Markdown("# 🧠 Noura the Document Checker ✈️ ")
gr.Markdown("Last Updated: June 10 2025")
file_input = gr.File(
label="Upload PDFs, Images or ZIP files (Multiple Supported)",
file_types=[".pdf", ".jpg", ".jpeg", ".png", ".zip"],
file_count="multiple"
)
with gr.Row():
tanggal_berangkat = gr.Textbox(
label="Tanggal Keberangkatan",
placeholder="Masukan Tanggal Keberangkatan",
type="text"
)
tanggal_pulang = gr.Textbox(
label="Tanggal Kepulangan",
placeholder="Masukan Tanggal Kepulangan",
type="text"
)
tanggal_biometrik = gr.Textbox(
label="Tanggal Biometrik",
placeholder="Masukan Tanggal Biometrik",
type="text"
)
dpi_slider = gr.Slider(
minimum=100,
maximum=400,
step=25,
label="Adjust DPI (100 - 400, Ξ”=25, default=350)",
value=350
)
run_btn = gr.Button("πŸƒ Run Analysis")
with gr.Row():
download_output_docx = gr.File(label="πŸ“₯ Download Summary as DOCX", visible=True)
# download_valid_zip = gr.File(label="πŸ“₯ Download all PDF document in zip", visible=True)
gr.Markdown("## πŸ“ FORM FILLING RESULT")
form_filling_output = gr.Textbox(label="πŸ“ FORM FILLING RESULT", lines=20)
gr.Markdown("## πŸ“ INVALID DOCUMENT LIST")
invalid_list_output = gr.Textbox(label="πŸ“ INVALID DOCUMENT LIST", lines=5)
gr.Markdown("## πŸ“ SUMMARY")
summary_output = gr.Textbox(label="πŸ“ SUMMARY OUTPUT", lines=5)
gr.Markdown("## πŸ“ NOTIFICATION MESSAGE")
notice_msg = gr.Textbox(label="πŸ“ NOTIFICATION MSG", lines=10)
gr.Markdown("## πŸ“ PER DOCUMENT ANALYSIS")
raw_output = gr.Textbox(label="πŸ“ PER DOCUMENT ANALYSIS", lines=20)
gr.Markdown("Token cost in IDR")
token_usage = gr.Textbox(label="Token cost in IDR", lines=5)
run_btn.click(
fn=main_process,
inputs=[file_input, tanggal_berangkat, tanggal_pulang, tanggal_biometrik, dpi_slider],
outputs=[download_output_docx, form_filling_output, invalid_list_output, raw_output, summary_output, notice_msg, token_usage]
)
demo.launch(debug=True)