Spaces:
Running
Running
| import json | |
| import os | |
| import base64 | |
| import hashlib | |
| import time | |
| from datetime import datetime | |
| from cryptography.fernet import Fernet | |
| from cryptography.hazmat.primitives import hashes | |
| from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
| from textblob import TextBlob | |
| from fpdf import FPDF | |
| from docx import Document | |
| import io | |
| import faiss | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| import secrets | |
| import string | |
| from google import genai # Gemini | |
| from dotenv import load_dotenv | |
| import nltk | |
| try: | |
| nltk.data.find('tokenizers/punkt_tab') | |
| except LookupError: | |
| nltk.download('punkt_tab') | |
| # This line reads the .env file and makes the API key available | |
| load_dotenv(override=True) # 'override=True' forces it to refresh the key if you changed .env | |
| NOTES_FILE = "notes.json" | |
| CONFIG_FILE = "vault_config.json" | |
| # Load a small, fast model for embeddings (runs locally) | |
| embed_model = SentenceTransformer('all-MiniLM-L6-v2') | |
| # --- 0. PIN & KEY LOGIC --- | |
| def get_pin_hash(pin: str): | |
| """Creates a secure SHA-256 hash of the PIN""" | |
| return hashlib.sha256(pin.encode()).hexdigest() | |
| def is_vault_initialized(): | |
| """Checks if a user has set a PIN yet""" | |
| return os.path.exists(CONFIG_FILE) | |
| def generate_recovery_key(): | |
| """Generates a random 16-character recovery string""" | |
| chars = string.ascii_uppercase + string.digits | |
| return ''.join(secrets.choice(chars) for _ in range(16)) | |
| def initialize_vault(pin: str, recovery_key: str): | |
| """Saves the user's initial PIN hash and recovery key hash""" | |
| with open(CONFIG_FILE, "w") as f: | |
| json.dump({"pin_hash": get_pin_hash(pin), | |
| "recovery_hash": get_pin_hash(recovery_key) # We hash the recovery key too! | |
| }, f) | |
| def verify_recovery_key(input_key: str): | |
| """Verifies the recovery key against the stored hash""" | |
| if not is_vault_initialized(): | |
| return False | |
| with open(CONFIG_FILE, "r") as f: | |
| stored_recovery_hash = json.load(f).get("recovery_hash") | |
| return get_pin_hash(input_key) == stored_recovery_hash | |
| def verify_pin(input_pin: str): | |
| """Verifies input against stored hash""" | |
| if not is_vault_initialized(): | |
| return False | |
| with open(CONFIG_FILE, "r") as f: | |
| stored_hash = json.load(f).get("pin_hash") | |
| return get_pin_hash(input_pin) == stored_hash | |
| # --- 1. KEY GENERATION (From your PIN) --- | |
| def generate_key(pin: str): | |
| """Derives a functional encryption key from the user's PIN""" | |
| password = pin.encode() | |
| # We use a static salt for this local project (Professional apps use random salts) | |
| salt = b'stable_salt_123' | |
| kdf = PBKDF2HMAC( | |
| algorithm=hashes.SHA256(), | |
| length=32, | |
| salt=salt, | |
| iterations=100000, | |
| ) | |
| key = base64.urlsafe_b64encode(kdf.derive(password)) | |
| return Fernet(key) | |
| # --- 2. ENCRYPTION / DECRYPTION --- | |
| def encrypt_data(data_string, pin): | |
| """Turns readable text into scrambled code""" | |
| f = generate_key(pin) | |
| return f.encrypt(data_string.encode()).decode() | |
| def decrypt_data(encrypted_string, pin): | |
| """Turns scrambled code back into readable text""" | |
| try: | |
| f = generate_key(pin) | |
| return f.decrypt(encrypted_string.encode()).decode() | |
| except Exception: | |
| return "[Decryption Error: Check PIN]" | |
| # --- 3. UPDATED LOAD/SAVE --- | |
| def load_notes(): | |
| if os.path.exists(NOTES_FILE): | |
| with open(NOTES_FILE, "r") as f: | |
| return json.load(f) | |
| return [] | |
| def save_notes(notes_list): | |
| with open(NOTES_FILE, "w") as f: | |
| json.dump(notes_list, f, indent=4) | |
| def get_page_heading(is_unlocked): | |
| return "🛡️ Safe Vault" if is_unlocked else "📝 My Notes" | |
| def get_filtered_notes(all_notes, is_unlocked, search_query): | |
| visible = all_notes | |
| if not is_unlocked: | |
| visible = [n for n in all_notes if not n.get('secret', False)] | |
| query = search_query.lower() | |
| return [n for n in visible if query in n['title'].lower() or query in n['content'].lower()] | |
| # --- 4. AI FEATURES --- | |
| def ai_summarize_text(text): | |
| """Uses NLP to extract key points from long notes.""" | |
| if len(text) < 50: | |
| return text # Don't summarize very short notes | |
| blob = TextBlob(text) | |
| # Extracting sentences and picking the top 2 for a summary | |
| sentences = [str(s) for s in blob.sentences] | |
| if len(sentences) > 2: | |
| summary = "AI Summary:\n" + "\n".join([f"- {s}" for s in sentences[:2]]) | |
| # NEW: Log the usage here! | |
| track_usage(text, type="input") | |
| track_usage(summary, type="output") | |
| return summary | |
| return text | |
| # --- 5. EXPORTS --- | |
| # The Logic Functions : These handle the actual file creation. | |
| def create_pdf(title, content): | |
| try: | |
| # 1. THE CLEANER: This removes long dashes, emojis, and special bullets | |
| # It replaces them with nothing or a standard space so the PDF doesn't crash | |
| clean_title = title.encode("ascii", "ignore").decode("ascii") | |
| clean_content = content.encode("ascii", "ignore").decode("ascii") | |
| pdf = FPDF() | |
| pdf.add_page() | |
| # 2. Use 'Arial' (the most stable font for Linux servers) | |
| pdf.set_font("Arial", "B", 16) | |
| pdf.multi_cell(0, 10, clean_title) | |
| pdf.ln(5) | |
| pdf.set_font("Arial", size=12) | |
| pdf.multi_cell(0, 10, clean_content) | |
| # 3. Stream output as a string first | |
| pdf_str = pdf.output(dest='S') | |
| # 4. Convert to BYTES (Streamlit buttons on HF MUST have bytes) | |
| if isinstance(pdf_str, str): | |
| return pdf_str.encode('latin-1') | |
| return pdf_str | |
| except Exception as e: | |
| # If there is still an error, this will show the message instead of crashing | |
| return f"ERROR: {str(e)}".encode('utf-8') | |
| def create_docx(title, content): | |
| doc = Document() | |
| doc.add_heading(title, 0) | |
| doc.add_paragraph(content) | |
| bio = io.BytesIO() | |
| doc.save(bio) | |
| return bio.getvalue() | |
| # --- 6. AI & RESOURCE TRACKER --- | |
| def track_usage(text, type="input"): | |
| # Rough estimation: 1 token approx 4 characters | |
| tokens = len(text) / 4 | |
| cost = (tokens / 1000) * 0.000125 # Estimate for Gemini 1.5 Flash | |
| # Save to a local JSON file to persist data | |
| usage_file = "usage_stats.json" | |
| stats = {"total_tokens": 0, "total_cost": 0.0} | |
| if os.path.exists(usage_file): | |
| with open(usage_file, "r") as f: | |
| stats = json.load(f) | |
| stats["total_tokens"] += tokens | |
| stats["total_cost"] += cost | |
| with open(usage_file, "w") as f: | |
| json.dump(stats, f) | |
| return stats | |
| # --- 7. RAG & FEEDBACK --- | |
| def create_vector_index(notes): | |
| """Turns notes into a searchable mathematical index""" | |
| if not notes: | |
| return None, [] | |
| # Extract only the text content | |
| text_data = [n['content'] for n in notes] | |
| # Convert text to vectors (embeddings) | |
| embeddings = embed_model.encode(text_data) | |
| # Create the FAISS index | |
| dimension = embeddings.shape[1] | |
| index = faiss.IndexFlatL2(dimension) | |
| index.add(np.array(embeddings).astype('float32')) | |
| return index, text_data | |
| def query_vault(query, index, text_data, top_k=2): | |
| """Finds the most relevant notes for a question""" | |
| if index is None: | |
| return "No notes found to search." | |
| # Convert question to vector | |
| query_vector = embed_model.encode([query]) | |
| # Search the index | |
| distances, indices = index.search(np.array(query_vector).astype('float32'), top_k) | |
| # Pull the relevant text chunks | |
| results = [text_data[i] for i in indices[0] if i != -1] | |
| return results | |
| def log_feedback(query, answer, context, status): | |
| log_file = "feedback_log.json" | |
| entry = { | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| "query": query, | |
| "answer": answer, | |
| "context_used": context, | |
| "status": status # "Correct" or "Wrong" | |
| } | |
| logs = [] | |
| if os.path.exists(log_file): | |
| with open(log_file, "r") as f: | |
| logs = json.load(f) | |
| logs.append(entry) | |
| with open(log_file, "w") as f: | |
| json.dump(logs, f, indent=4) | |
| # --- 8. GEMINI AI ENGINE --- | |
| def get_gemini_response(user_query, context_str): | |
| """Connects to Google Gemini API for free AI logic""" | |
| api_key = os.getenv("GEMINI_API_KEY") | |
| if not api_key: | |
| return "⚠️ Error: GEMINI_API_KEY not found in Hugging Face Secrets." | |
| try: | |
| # New initialization style | |
| client = genai.Client(api_key=api_key) | |
| prompt = f""" | |
| You are a secure vault assistant. Use the following retrieved notes to answer. | |
| Notes Context: {context_str} | |
| User Question: {user_query} | |
| """ | |
| # Inside your loop or before you call the model | |
| time.sleep(2) # Wait 2 seconds between requests | |
| # Simplified model call | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash-lite", | |
| contents=prompt | |
| ) | |
| track_usage(prompt, type="input") | |
| track_usage(response.text, type="output") | |
| return response.text | |
| except Exception as e: | |
| return f"❌ AI Engine Error: {str(e)}" |