Spaces:
Runtime error
Runtime error
Commit
·
23d4b72
1
Parent(s):
1aefa24
upload files to huggingface
Browse files- Dockerfile +22 -0
- README.md +26 -8
- app/__init__.py +0 -0
- app/ai_processor.py +90 -0
- app/main.py +125 -0
- app/models.py +5 -0
- requirements.txt +10 -0
Dockerfile
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim
|
2 |
+
|
3 |
+
#variabel
|
4 |
+
ENV PYTHONUNBUFFERED 1
|
5 |
+
#local char
|
6 |
+
ENV LANG C.UTF-8
|
7 |
+
#directory
|
8 |
+
WORKDIR /app
|
9 |
+
|
10 |
+
#copyfile requirements
|
11 |
+
COPY ./requirements.txt /app/requirements.txt
|
12 |
+
|
13 |
+
#install depedensi
|
14 |
+
RUN pip install --no-cache-dir --upgrade pip \
|
15 |
+
&& pip install --no-cache-dir --prefer-binary -r /app/requirements.txt
|
16 |
+
#copy aplikasi ke folder
|
17 |
+
COPY ./app /app/app
|
18 |
+
#port
|
19 |
+
EXPOSE 8000
|
20 |
+
|
21 |
+
#running
|
22 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
README.md
CHANGED
@@ -1,12 +1,30 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
|
|
7 |
pinned: false
|
8 |
-
license: mit
|
9 |
-
short_description: AI summarize
|
10 |
-
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: SmartCV Backend API
|
3 |
+
emoji: बुद्धि
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: green
|
6 |
sdk: docker
|
7 |
+
app_port: 8000
|
8 |
pinned: false
|
|
|
|
|
|
|
9 |
|
10 |
+
# SmartCV Backend API
|
11 |
+
|
12 |
+
Backend API untuk aplikasi SmartCV.
|
13 |
+
Dibangun dengan Python, FastAPI, dan menggunakan model AI dari Hugging Face Transformers untuk meringkas teks pengalaman kerja menjadi format CV yang profesional.
|
14 |
+
|
15 |
+
**Endpoint Utama:**
|
16 |
+
- `POST /summarize`: Menerima JSON dengan field `text` dan mengembalikan JSON dengan field `summary`.
|
17 |
+
|
18 |
+
**Teknologi:**
|
19 |
+
- Python 3.10
|
20 |
+
- FastAPI
|
21 |
+
- Uvicorn
|
22 |
+
- Pydantic
|
23 |
+
- Hugging Face Transformers (Model: [NAMA_MODEL_ANDA, misal google/flan-t5-small])
|
24 |
+
|
25 |
+
## Cara Menjalankan Lokal (Untuk Pengembangan)
|
26 |
+
1. `pip install -r requirements.txt`
|
27 |
+
2. `uvicorn app.main:app --reload --port 8000`
|
28 |
+
|
29 |
+
## Deployment
|
30 |
+
Dideploy ke Hugging Face Spaces menggunakan Docker.
|
app/__init__.py
ADDED
File without changes
|
app/ai_processor.py
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# backend/app/ai_processor.py
|
2 |
+
import torch
|
3 |
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
4 |
+
import os
|
5 |
+
import traceback # Untuk logging error
|
6 |
+
|
7 |
+
# --- MODEL_NAME tetap sama seperti yang sudah Anda pilih dan uji ---
|
8 |
+
MODEL_NAME = "google/flan-t5-small"
|
9 |
+
# MODEL_NAME = "facebook/bart-large-cnn"
|
10 |
+
# MODEL_NAME = "sshleifer/distilbart-cnn-6-6"
|
11 |
+
|
12 |
+
model = None
|
13 |
+
tokenizer = None
|
14 |
+
device = None
|
15 |
+
|
16 |
+
def initialize_model():
|
17 |
+
global model, tokenizer, device
|
18 |
+
if model is not None and tokenizer is not None:
|
19 |
+
# print("INFO: AI Processor - Model dan tokenizer sudah dimuat.") # Bisa di-uncomment jika perlu
|
20 |
+
return True
|
21 |
+
try:
|
22 |
+
print(f"INFO: AI Processor - Memulai proses pemuatan model: {MODEL_NAME}...")
|
23 |
+
if torch.cuda.is_available():
|
24 |
+
device = torch.device("cuda")
|
25 |
+
print("INFO: AI Processor - GPU (CUDA) terdeteksi.")
|
26 |
+
else:
|
27 |
+
device = torch.device("cpu")
|
28 |
+
print("INFO: AI Processor - GPU tidak terdeteksi, menggunakan CPU.")
|
29 |
+
|
30 |
+
print(f"INFO: AI Processor - Memuat tokenizer untuk {MODEL_NAME}...")
|
31 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
32 |
+
print("INFO: AI Processor - Tokenizer berhasil dimuat.")
|
33 |
+
|
34 |
+
print(f"INFO: AI Processor - Memuat model {MODEL_NAME} ke device {device}...")
|
35 |
+
model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME)
|
36 |
+
model.to(device)
|
37 |
+
model.eval()
|
38 |
+
print(f"INFO: AI Processor - Model {MODEL_NAME} berhasil dimuat ke {device}.")
|
39 |
+
return True
|
40 |
+
except Exception as e:
|
41 |
+
print(f"ERROR: AI Processor - Gagal memuat model {MODEL_NAME}: {str(e)}")
|
42 |
+
traceback.print_exc()
|
43 |
+
model = None
|
44 |
+
tokenizer = None
|
45 |
+
return False
|
46 |
+
|
47 |
+
def generate_cv_summary(input_text: str) -> str:
|
48 |
+
global model, tokenizer, device
|
49 |
+
if model is None or tokenizer is None:
|
50 |
+
error_msg = "ERROR: AI Processor - Model atau tokenizer belum berhasil diinisialisasi. Coba panggil initialize_model() lagi."
|
51 |
+
print(error_msg)
|
52 |
+
if not initialize_model(): # Coba inisialisasi ulang
|
53 |
+
return error_msg + " Inisialisasi ulang juga gagal."
|
54 |
+
if model is None or tokenizer is None: # Cek lagi setelah coba inisialisasi ulang
|
55 |
+
return "ERROR: AI Processor - Model tetap tidak tersedia setelah mencoba inisialisasi ulang."
|
56 |
+
|
57 |
+
# Log ini bisa dipertahankan untuk melihat apa yang diproses
|
58 |
+
# print(f"INFO: AI Processor - Menerima teks untuk diringkas (panjang: {len(input_text)} char).")
|
59 |
+
try:
|
60 |
+
# --- PROMPT ANDA YANG SUDAH DISESUAIKAN ---
|
61 |
+
prompt_prefix = "Summarize the following work experience for a professional, ATS-friendly CV. Focus on quantifiable achievements, key responsibilities, and relevant skills. Use concise bullet points if appropriate: "
|
62 |
+
# Atau prompt lain yang sudah Anda temukan bekerja dengan baik.
|
63 |
+
# -----------------------------------------
|
64 |
+
text_to_summarize = prompt_prefix + input_text
|
65 |
+
# print(f"DEBUG: AI Processor - Teks input ke tokenizer: '{text_to_summarize[:100]}...'") # Hapus jika terlalu verbose
|
66 |
+
|
67 |
+
inputs = tokenizer(text_to_summarize, return_tensors="pt", max_length=1024, truncation=True, padding="longest")
|
68 |
+
input_ids = inputs.input_ids.to(device)
|
69 |
+
attention_mask = inputs.attention_mask.to(device)
|
70 |
+
|
71 |
+
# print(f"DEBUG: AI Processor - Melakukan inferensi pada device {device}...") # Hapus jika terlalu verbose
|
72 |
+
with torch.no_grad():
|
73 |
+
summary_ids = model.generate(
|
74 |
+
input_ids,
|
75 |
+
attention_mask=attention_mask,
|
76 |
+
max_length=250,
|
77 |
+
min_length=50,
|
78 |
+
num_beams=4,
|
79 |
+
early_stopping=True,
|
80 |
+
no_repeat_ngram_size=3,
|
81 |
+
)
|
82 |
+
summary_text = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
|
83 |
+
# print(f"INFO: AI Processor - Ringkasan digenerate: '{summary_text[:100]}...'") # Bisa dipertahankan
|
84 |
+
return summary_text.strip()
|
85 |
+
except Exception as e:
|
86 |
+
print(f"ERROR: AI Processor - Error saat proses generasi ringkasan: {str(e)}")
|
87 |
+
traceback.print_exc()
|
88 |
+
return "Error: Terjadi masalah internal pada AI saat mencoba membuat ringkasan. Silakan coba lagi."
|
89 |
+
|
90 |
+
# initialize_model() akan dipanggil dari startup event di main.py
|
app/main.py
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# backend/app/main.py
|
2 |
+
from fastapi import FastAPI, HTTPException
|
3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
4 |
+
import os
|
5 |
+
import traceback # Untuk logging traceback error yang lebih detail
|
6 |
+
|
7 |
+
# Pastikan import dari modul lokal Anda benar
|
8 |
+
from .models import TextInput, SummaryOutput
|
9 |
+
from .ai_processor import generate_cv_summary, initialize_model as initialize_ai_model
|
10 |
+
|
11 |
+
# python-dotenv hanya berguna untuk pengembangan lokal jika ada .env file.
|
12 |
+
# Di produksi (HF Spaces), variabel lingkungan diset melalui secrets.
|
13 |
+
# from dotenv import load_dotenv
|
14 |
+
# load_dotenv()
|
15 |
+
|
16 |
+
app = FastAPI(
|
17 |
+
title="SmartCV API - Production", # Ganti title jika mau
|
18 |
+
description="API untuk menghasilkan ringkasan CV profesional menggunakan AI.",
|
19 |
+
version="1.0.0" # Ganti versi jika mau
|
20 |
+
)
|
21 |
+
|
22 |
+
# --- Konfigurasi CORS untuk Produksi dan Pengembangan ---
|
23 |
+
# Daftar origins default yang selalu diizinkan (untuk dev lokal)
|
24 |
+
allowed_origins_core = [
|
25 |
+
"http://localhost:3000",
|
26 |
+
"http://localhost:3001", # Jika Anda kadang menggunakan port lain untuk frontend dev
|
27 |
+
]
|
28 |
+
|
29 |
+
# Ambil URL frontend dari environment variables (diset di HF Spaces atau Vercel)
|
30 |
+
# Untuk HF Spaces, kita akan set VERCEL_FRONTEND_URL sebagai secret
|
31 |
+
vercel_url_from_env = os.getenv("VERCEL_FRONTEND_URL")
|
32 |
+
if vercel_url_from_env:
|
33 |
+
# Pastikan tidak ada spasi dan hapus trailing slash jika ada
|
34 |
+
cleaned_vercel_url = vercel_url_from_env.strip().rstrip('/')
|
35 |
+
if cleaned_vercel_url: # Pastikan tidak kosong setelah strip
|
36 |
+
allowed_origins_core.append(cleaned_vercel_url)
|
37 |
+
print(f"INFO: Backend - Vercel frontend URL '{cleaned_vercel_url}' ditambahkan ke CORS.")
|
38 |
+
else:
|
39 |
+
print("WARN: Backend - VERCEL_FRONTEND_URL diset tapi kosong setelah dibersihkan.")
|
40 |
+
|
41 |
+
|
42 |
+
# Ambil URL Gitpod untuk pengembangan (jika berjalan di Gitpod)
|
43 |
+
gitpod_workspace_url_from_env = os.getenv("GITPOD_WORKSPACE_URL")
|
44 |
+
if gitpod_workspace_url_from_env:
|
45 |
+
frontend_port_gitpod = 3000 # Asumsi port frontend React di Gitpod
|
46 |
+
# Hapus "https://" sebelum membangun URL port
|
47 |
+
gitpod_domain_part = gitpod_workspace_url_from_env.replace('https://', '', 1)
|
48 |
+
gitpod_frontend_origin = f"https://{frontend_port_gitpod}-{gitpod_domain_part}"
|
49 |
+
allowed_origins_core.append(gitpod_frontend_origin)
|
50 |
+
print(f"INFO: Backend - Gitpod frontend URL '{gitpod_frontend_origin}' ditambahkan ke CORS.")
|
51 |
+
|
52 |
+
# Jika tidak ada URL produksi (Vercel) atau Gitpod yang valid terdeteksi selain localhost,
|
53 |
+
# mungkin lebih aman untuk TIDAK mengizinkan "*" di produksi.
|
54 |
+
# Namun, untuk portofolio ini, jika VERCEL_FRONTEND_URL tidak diset,
|
55 |
+
# mungkin kita perlu fallback ke "*" agar tidak error saat pertama kali deploy sebelum set secret.
|
56 |
+
# Ini adalah trade-off keamanan vs kemudahan setup awal.
|
57 |
+
|
58 |
+
# Hapus duplikat jika ada
|
59 |
+
final_allowed_origins = sorted(list(set(allowed_origins_core)))
|
60 |
+
|
61 |
+
if not any(origin.startswith("https://") for origin in final_allowed_origins if origin not in ["http://localhost:3000", "http://localhost:3001"]):
|
62 |
+
# Jika tidak ada origin HTTPS (Vercel/Gitpod) yang valid, cetak peringatan.
|
63 |
+
# Untuk produksi, idealnya ini tidak terjadi.
|
64 |
+
print("WARN: Backend - Tidak ada origin HTTPS (Vercel/Gitpod) yang dikonfigurasi untuk CORS selain localhost. Ini mungkin tidak aman untuk produksi.")
|
65 |
+
# Jika Anda ingin lebih ketat di produksi dan VERCEL_FRONTEND_URL WAJIB ada:
|
66 |
+
# if not vercel_url_from_env:
|
67 |
+
# print("CRITICAL: VERCEL_FRONTEND_URL tidak diset! CORS mungkin tidak berfungsi untuk frontend produksi.")
|
68 |
+
# # Anda bisa memilih untuk raise error di sini atau biarkan (akan fallback ke localhost saja)
|
69 |
+
|
70 |
+
print(f"INFO: Backend - CORS akan mengizinkan origins: {final_allowed_origins}")
|
71 |
+
|
72 |
+
app.add_middleware(
|
73 |
+
CORSMiddleware,
|
74 |
+
allow_origins=final_allowed_origins if final_allowed_origins else ["http://localhost:3000"], # Fallback minimal jika daftar kosong
|
75 |
+
allow_credentials=True,
|
76 |
+
allow_methods=["GET", "POST", "OPTIONS"],
|
77 |
+
allow_headers=["*"], # Untuk kesederhanaan, izinkan semua header. Bisa diperketat.
|
78 |
+
)
|
79 |
+
# --- Akhir Konfigurasi CORS ---
|
80 |
+
|
81 |
+
@app.on_event("startup")
|
82 |
+
async def startup_event():
|
83 |
+
print("INFO: Backend - Aplikasi FastAPI memulai proses startup...")
|
84 |
+
if initialize_ai_model():
|
85 |
+
print("INFO: Backend - Model AI berhasil diinisialisasi atau sudah siap.")
|
86 |
+
else:
|
87 |
+
# Ini adalah log error penting untuk produksi
|
88 |
+
print("ERROR: Backend - Model AI GAGAL diinisialisasi saat startup! Endpoint AI tidak akan berfungsi.")
|
89 |
+
|
90 |
+
@app.get("/", include_in_schema=False) # Sembunyikan dari docs API jika mau
|
91 |
+
async def read_root():
|
92 |
+
# Log ini bisa berguna untuk health check sederhana
|
93 |
+
# print("INFO: Backend - Root endpoint '/' diakses.")
|
94 |
+
return {"message": "Selamat datang di SmartCV API! API aktif."}
|
95 |
+
|
96 |
+
@app.post("/summarize", response_model=SummaryOutput)
|
97 |
+
async def summarize_text_endpoint(input_data: TextInput):
|
98 |
+
# Log ini penting untuk melihat traffic
|
99 |
+
print(f"INFO: Backend - Menerima permintaan ke /summarize. Panjang input: {len(input_data.text)} char.")
|
100 |
+
|
101 |
+
if not input_data.text or not input_data.text.strip() or len(input_data.text) < 10 : # Tambahkan min_length di sini juga
|
102 |
+
print(f"WARN: Backend - Input teks tidak valid atau terlalu pendek. Input: '{input_data.text[:30]}...'")
|
103 |
+
raise HTTPException(status_code=400, detail="Input teks tidak boleh kosong dan minimal 10 karakter.")
|
104 |
+
|
105 |
+
try:
|
106 |
+
# print("DEBUG: Backend - Memanggil generate_cv_summary...") # Bisa dihapus
|
107 |
+
summary = generate_cv_summary(input_data.text)
|
108 |
+
# print(f"DEBUG: Backend - Hasil dari generate_cv_summary: '{summary[:50]}...'") # Bisa dihapus
|
109 |
+
|
110 |
+
if summary.startswith("Error:"): # Cek jika fungsi AI mengembalikan pesan error internal
|
111 |
+
print(f"ERROR: Backend - Error dari ai_processor: {summary}")
|
112 |
+
# Untuk error dari AI processor, mungkin 500 lebih cocok daripada 503 jika itu error pemrosesan
|
113 |
+
raise HTTPException(status_code=500, detail=summary)
|
114 |
+
|
115 |
+
# print("INFO: Backend - Ringkasan berhasil dibuat, mengirim respons.") # Bisa dihapus jika terlalu verbose
|
116 |
+
return SummaryOutput(summary=summary)
|
117 |
+
except HTTPException:
|
118 |
+
# Re-raise HTTPException agar FastAPI menanganinya dengan benar (misal, 400 atau 503 dari atas)
|
119 |
+
raise
|
120 |
+
except Exception as e:
|
121 |
+
# Ini menangkap error tak terduga lainnya dari dalam endpoint
|
122 |
+
print(f"CRITICAL: Backend - Error tidak terduga di endpoint /summarize: {str(e)}")
|
123 |
+
# Cetak traceback lengkap ke log server untuk debugging mendalam
|
124 |
+
traceback.print_exc()
|
125 |
+
raise HTTPException(status_code=500, detail=f"Terjadi kesalahan internal server saat memproses permintaan Anda.")
|
app/models.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
class TextInput(BaseModel):
|
3 |
+
text: str = Field(..., min_length=10, description="Teks input dari pengguna yang akan diringkas.")
|
4 |
+
class SummaryOutput(BaseModel):
|
5 |
+
summary: str = Field(..., description="Hasil ringkasan teks yang dihasilkan oleh AI.")
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
uvicorn[standard] #dukungan websockets dan lain-lain
|
3 |
+
pydantic
|
4 |
+
python-dotenv #.env lokal
|
5 |
+
|
6 |
+
#depedensi AI
|
7 |
+
transformers[torch]
|
8 |
+
# torch
|
9 |
+
sentencepiece
|
10 |
+
accelerate
|