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)