AI Resume Ranker

import os

import re

import argparse

import glob

from typing import List, Tuple, Dict, Optional


import pandas as pd

import numpy as np


from sklearn.feature_extraction.text import TfidfVectorizer, ENGLISH_STOP_WORDS

from sklearn.metrics.pairwise import cosine_similarity


# Optional parsers

import docx2txt

import PyPDF2


# Optional NLP

try:

    import spacy

    SPACY_OK = True

except Exception:

    SPACY_OK = False



# --------------------------- File Readers ---------------------------


def read_txt(path: str) -> str:

    with open(path, "r", encoding="utf-8", errors="ignore") as f:

        return f.read()


def read_docx(path: str) -> str:

    try:

        return docx2txt.process(path) or ""

    except Exception:

        return ""


def read_pdf(path: str) -> str:

    text = []

    try:

        with open(path, "rb") as f:

            reader = PyPDF2.PdfReader(f)

            for page in reader.pages:

                t = page.extract_text() or ""

                text.append(t)

    except Exception:

        pass

    return "\n".join(text)


def load_text_any(path: str) -> str:

    ext = os.path.splitext(path)[1].lower()

    if ext == ".txt":

        return read_txt(path)

    elif ext == ".docx":

        return read_docx(path)

    elif ext == ".pdf":

        return read_pdf(path)

    else:

        return ""



# --------------------------- Skills ---------------------------


DEFAULT_SKILLS = [

    # Generic

    "python","java","c++","javascript","typescript","sql","nosql","git","docker","kubernetes","linux",

    "aws","azure","gcp","bash","shell","rest","graphql","microservices",

    # Data/AI

    "pandas","numpy","scikit-learn","sklearn","tensorflow","pytorch","keras","nltk","spacy",

    "spark","hadoop","airflow","dbt","powerbi","tableau","matplotlib","seaborn",

    # Web/Backend

    "django","flask","fastapi","spring","node","express","react","angular","vue",

    # DevOps/Cloud

    "terraform","ansible","jenkins","ci/cd","prometheus","grafana","elk","rabbitmq","kafka",

    # Testing

    "pytest","unittest","selenium","cypress",

    # Security & Other

    "oauth","jwt","scrum","agile","jira"

]


def load_skills_file(path: Optional[str]) -> List[str]:

    if not path:

        return DEFAULT_SKILLS

    skills = []

    with open(path, "r", encoding="utf-8", errors="ignore") as f:

        for line in f:

            s = line.strip().lower()

            if s:

                skills.append(s)

    return sorted(set(skills))



# --------------------------- NLP Cleaning ---------------------------


def build_spacy_pipeline(use_spacy: bool):

    if use_spacy and SPACY_OK:

        try:

            nlp = spacy.load("en_core_web_sm", disable=["ner","parser","textcat"])

            return nlp

        except Exception:

            return None

    return None


CLEAN_RE = re.compile(r"[^a-z0-9+#./\- ]+")


def normalize_text(text: str) -> str:

    text = text.lower()

    text = text.replace("\n", " ").replace("\t", " ")

    text = CLEAN_RE.sub(" ", text)

    text = re.sub(r"\s+", " ", text).strip()

    return text


def lemmatize_spacy(nlp, text: str) -> str:

    if not nlp:

        return text

    doc = nlp(text)

    return " ".join(tok.lemma_ for tok in doc if not tok.is_space)



# --------------------------- Feature Engineering ---------------------------


def skill_overlap_score(text: str, jd_skills: List[str]) -> float:

    """

    Compute a skill overlap score (0..1) = Jaccard-like:

    |skills_in_resume ∩ skills_in_jd| / |skills_in_jd|

    """

    text_tokens = set(re.findall(r"[a-z0-9+#.\-]+", text.lower()))

    resume_skills = set()

    for skill in jd_skills:

        tokens = skill.split()

        if len(tokens) == 1:

            if skill in text_tokens:

                resume_skills.add(skill)

        else:

            if skill in text:

                resume_skills.add(skill)

    if not jd_skills:

        return 0.0

    return len(resume_skills) / float(len(set(jd_skills)))



# --------------------------- Ranking ---------------------------


def rank_resumes(

    jd_text: str,

    resume_texts: Dict[str, str],

    use_spacy: bool = False,

    weights: Tuple[float, float] = (0.7, 0.3),

    custom_skills: Optional[List[str]] = None

) -> pd.DataFrame:


    w_sem, w_skill = weights

    assert abs((w_sem + w_skill) - 1.0) < 1e-6, "weights must sum to 1"


    # Prepare spaCy if requested

    nlp = build_spacy_pipeline(use_spacy)


    # Normalize & (optionally) lemmatize

    jd_clean = normalize_text(jd_text)

    if nlp:

        jd_clean = lemmatize_spacy(nlp, jd_clean)


    cleaned = {}

    for fname, txt in resume_texts.items():

        t = normalize_text(txt)

        if nlp:

            t = lemmatize_spacy(nlp, t)

        cleaned[fname] = t


    # TF-IDF across JD + Resumes

    vectorizer = TfidfVectorizer(stop_words="english", max_features=40000, ngram_range=(1,2))

    corpus = [jd_clean] + [cleaned[f] for f in cleaned]

    tfidf = vectorizer.fit_transform(corpus)


    # Cosine similarity of resumes against JD (index 0)

    sims = cosine_similarity(tfidf[0:1], tfidf[1:]).flatten()


    # Skills from JD text (intersect default skills + those present in JD)

    base_skills = custom_skills if custom_skills is not None else DEFAULT_SKILLS

    jd_skill_candidates = [s for s in base_skills if s in jd_clean]

    # Fallback: if no skills found in JD, keep base set (less strict)

    jd_skillset = jd_skill_candidates if jd_skill_candidates else base_skills


    # Skill overlap score per resume

    files = list(cleaned.keys())

    skill_scores = []

    for f in files:

        s = skill_overlap_score(cleaned[f], jd_skillset)

        skill_scores.append(s)


    # Final score

    final = w_sem * sims + w_skill * np.array(skill_scores)


    df = pd.DataFrame({

        "resume_file": files,

        "semantic_similarity": np.round(sims, 4),

        "skill_overlap": np.round(skill_scores, 4),

        "final_score": np.round(final, 4)

    }).sort_values("final_score", ascending=False).reset_index(drop=True)


    return df



# --------------------------- CLI ---------------------------


def main():

    parser = argparse.ArgumentParser(description="AI Resume Ranker — rank resumes against a job description.")

    parser.add_argument("--jd", required=True, help="Job description file (.txt/.pdf/.docx)")

    parser.add_argument("--resumes", required=True, help="Folder containing resumes (.txt/.pdf/.docx)")

    parser.add_argument("--export", default="ranked_resumes.csv", help="Path to export CSV results")

    parser.add_argument("--skills", default=None, help="Optional skills file (one skill per line)")

    parser.add_argument("--use-spacy", action="store_true", help="Enable spaCy lemmatization (install en_core_web_sm)")

    parser.add_argument("--weights", nargs=2, type=float, default=[0.7, 0.3],

                        help="Weights for [semantic_similarity skill_overlap], must sum to 1.0 (default 0.7 0.3)")

    args = parser.parse_args()


    # Load JD

    jd_text = load_text_any(args.jd)

    if not jd_text.strip():

        raise SystemExit(f"Could not read job description: {args.jd}")


    # Load resumes

    patterns = ["*.pdf", "*.docx", "*.txt"]

    files = []

    for p in patterns:

        files.extend(glob.glob(os.path.join(args.resumes, p)))

    if not files:

        raise SystemExit(f"No resumes found in: {args.resumes}")


    resume_texts = {}

    for f in files:

        txt = load_text_any(f)

        if txt.strip():

            resume_texts[os.path.basename(f)] = txt


    # Load skills (optional)

    custom_skills = load_skills_file(args.skills) if args.skills else None


    # Rank

    df = rank_resumes(

        jd_text=jd_text,

        resume_texts=resume_texts,

        use_spacy=args.use_spacy,

        weights=(args.weights[0], args.weights[1]),

        custom_skills=custom_skills

    )


    # Save

    df.to_csv(args.export, index=False)

    print("\nTop matches:")

    print(df.head(10).to_string(index=False))

    print(f"\nSaved results to: {args.export}")



if __name__ == "__main__":

    main()


Interactive Periodic Table

import json

import os

import tkinter as tk

from tkinter import ttk, messagebox

import webbrowser

from functools import partial

import threading


import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 unused import (needed by matplotlib 3D)

import numpy as np


# ----------------- Config -----------------

DATA_FILE = "elements.json"

GRID_COLUMNS = 18

GRID_ROWS = 9  # we'll use rows 1-7 + an extra for Lanthanides/Actinides placeholder


# ----------------- Default JSON Creator -----------------

DEFAULT_ELEMENTS = {

    # symbol: data

    "H": {"name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008, "group": 1, "period": 1,

          "category": "Nonmetal", "uses": ["Fuel", "Chemical feedstock"], "electron_configuration": "1s1"},

    "He": {"name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026, "group": 18, "period": 1,

           "category": "Noble Gas", "uses": ["Balloons", "Cryogenics"], "electron_configuration": "1s2"},

    "Li": {"name": "Lithium", "atomic_number": 3, "atomic_mass": 6.94, "group": 1, "period": 2,

           "category": "Alkali Metal", "uses": ["Batteries"], "electron_configuration": "[He] 2s1"},

    "Be": {"name": "Beryllium", "atomic_number": 4, "atomic_mass": 9.0122, "group": 2, "period": 2,

           "category": "Alkaline Earth Metal", "uses": ["Aerospace alloys"], "electron_configuration": "[He] 2s2"},

    "B": {"name": "Boron", "atomic_number": 5, "atomic_mass": 10.81, "group": 13, "period": 2,

          "category": "Metalloid", "uses": ["Glass", "Detergents"], "electron_configuration": "[He] 2s2 2p1"},

    "C": {"name": "Carbon", "atomic_number": 6, "atomic_mass": 12.011, "group": 14, "period": 2,

          "category": "Nonmetal", "uses": ["Organic chemistry", "Materials"], "electron_configuration": "[He] 2s2 2p2"},

    "N": {"name": "Nitrogen", "atomic_number": 7, "atomic_mass": 14.007, "group": 15, "period": 2,

          "category": "Nonmetal", "uses": ["Fertilizers", "Industrial gas"], "electron_configuration": "[He] 2s2 2p3"},

    "O": {"name": "Oxygen", "atomic_number": 8, "atomic_mass": 15.999, "group": 16, "period": 2,

          "category": "Nonmetal", "uses": ["Respiration", "Combustion"], "electron_configuration": "[He] 2s2 2p4"},

    "F": {"name": "Fluorine", "atomic_number": 9, "atomic_mass": 18.998, "group": 17, "period": 2,

          "category": "Halogen", "uses": ["Toothpaste (fluoride)"], "electron_configuration": "[He] 2s2 2p5"},

    "Ne": {"name": "Neon", "atomic_number": 10, "atomic_mass": 20.1797, "group": 18, "period": 2,

           "category": "Noble Gas", "uses": ["Neon signs"], "electron_configuration": "[He] 2s2 2p6"},

    "Na": {"name": "Sodium", "atomic_number": 11, "atomic_mass": 22.989, "group": 1, "period": 3,

           "category": "Alkali Metal", "uses": ["Salt", "Chemicals"], "electron_configuration": "[Ne] 3s1"},

    "Mg": {"name": "Magnesium", "atomic_number": 12, "atomic_mass": 24.305, "group": 2, "period": 3,

           "category": "Alkaline Earth Metal", "uses": ["Alloys", "Electronics"], "electron_configuration": "[Ne] 3s2"},

    "Al": {"name": "Aluminium", "atomic_number": 13, "atomic_mass": 26.9815, "group": 13, "period": 3,

           "category": "Post-transition Metal", "uses": ["Packaging", "Aerospace"], "electron_configuration": "[Ne] 3s2 3p1"},

    "Si": {"name": "Silicon", "atomic_number": 14, "atomic_mass": 28.085, "group": 14, "period": 3,

           "category": "Metalloid", "uses": ["Semiconductors"], "electron_configuration": "[Ne] 3s2 3p2"},

    "P": {"name": "Phosphorus", "atomic_number": 15, "atomic_mass": 30.9738, "group": 15, "period": 3,

          "category": "Nonmetal", "uses": ["Fertilizers"], "electron_configuration": "[Ne] 3s2 3p3"},

    "S": {"name": "Sulfur", "atomic_number": 16, "atomic_mass": 32.06, "group": 16, "period": 3,

          "category": "Nonmetal", "uses": ["Sulfuric acid", "Fertilizers"], "electron_configuration": "[Ne] 3s2 3p4"},

    "Cl": {"name": "Chlorine", "atomic_number": 17, "atomic_mass": 35.45, "group": 17, "period": 3,

           "category": "Halogen", "uses": ["Water purification"], "electron_configuration": "[Ne] 3s2 3p5"},

    "Ar": {"name": "Argon", "atomic_number": 18, "atomic_mass": 39.948, "group": 18, "period": 3,

           "category": "Noble Gas", "uses": ["Lighting", "Welding"], "electron_configuration": "[Ne] 3s2 3p6"},

    "K": {"name": "Potassium", "atomic_number": 19, "atomic_mass": 39.0983, "group": 1, "period": 4,

          "category": "Alkali Metal", "uses": ["Fertilizers"], "electron_configuration": "[Ar] 4s1"},

    "Ca": {"name": "Calcium", "atomic_number": 20, "atomic_mass": 40.078, "group": 2, "period": 4,

           "category": "Alkaline Earth Metal", "uses": ["Bones", "Construction"], "electron_configuration": "[Ar] 4s2"}

}


def ensure_data_file():

    if not os.path.exists(DATA_FILE):

        with open(DATA_FILE, "w", encoding="utf-8") as f:

            json.dump(DEFAULT_ELEMENTS, f, indent=2)

        print(f"Created default {DATA_FILE} with first 20 elements. You can edit/expand it.")


# ----------------- Utility: Load Data -----------------

def load_elements():

    ensure_data_file()

    with open(DATA_FILE, "r", encoding="utf-8") as f:

        data = json.load(f)

    # Normalize: attach group/period if missing

    elements = {}

    for sym, info in data.items():

        elements[sym] = info

    return elements


# ----------------- Orbital Visualizer -----------------

def show_orbital_visual(element_data):

    """

    Very simple 3D shells visualization.

    Each shell (n) is drawn as a translucent sphere surface of radius proportional to n.

    Electron counts per shell approximated from electron_configuration (simple parse).

    """

    # parse approximate shells from electron_configuration string by counting numbers in brackets or 's','p','d'

    cfg = element_data.get("electron_configuration", "")

    # crude parsing to get principal quantum numbers occurrences like 1s2 2s2 2p6 etc.

    shells = []

    import re

    matches = re.findall(r'(\d)(?:[spdf])\d*', cfg)

    if matches:

        # get unique shell numbers and sort

        shells = sorted(set(int(m) for m in matches))

    else:

        # fallback: estimate shells from atomic number

        an = element_data.get("atomic_number", 1)

        # very rough heuristic: shell count = ceil(sqrt(Z)/2) — just for visualization

        shells = list(range(1, int(max(1, round((an**0.5)/2))) + 2))


    fig = plt.figure(figsize=(6,6))

    ax = fig.add_subplot(111, projection='3d')

    ax.set_facecolor("white")

    ax._axis3don = False


    # Draw concentric sphere surfaces for shells

    for n in shells:

        # create sphere

        u = np.linspace(0, 2 * np.pi, 60)

        v = np.linspace(0, np.pi, 30)

        r = 0.6 * n  # radius scale

        x = r * np.outer(np.cos(u), np.sin(v))

        y = r * np.outer(np.sin(u), np.sin(v))

        z = r * np.outer(np.ones_like(u), np.cos(v))

        ax.plot_surface(x, y, z, color=plt.cm.viridis(n/ max(1, len(shells))), alpha=0.15, linewidth=0)

        # put some electron points randomly distributed on shell

        np.random.seed(n*10)

        m = min(12, 4 + n*4)

        theta = np.random.uniform(0, 2*np.pi, m)

        phi = np.random.uniform(0, np.pi, m)

        ex = r * np.sin(phi) * np.cos(theta)

        ey = r * np.sin(phi) * np.sin(theta)

        ez = r * np.cos(phi)

        ax.scatter(ex, ey, ez, s=30, color=plt.cm.viridis(n / max(1, len(shells))))


    ax.set_title(f"Shells for {element_data.get('name', '')} ({element_data.get('symbol','')})", pad=20)

    max_r = 0.6 * (max(shells) + 1)

    ax.set_xlim(-max_r, max_r)

    ax.set_ylim(-max_r, max_r)

    ax.set_zlim(-max_r, max_r)

    plt.show()


# ----------------- GUI -----------------

class PeriodicTableApp:

    def __init__(self, root):

        self.root = root

        self.root.title("Interactive Periodic Table")

        self.elements = load_elements()  # key: symbol

        # create a mapping by atomic_number for quick lookup

        self.by_atomic = {info["atomic_number"]: sym for sym, info in self.elements.items() if "atomic_number" in info}

        self.buttons = {}  # symbol -> button

        self.build_ui()


    def build_ui(self):

        top = tk.Frame(self.root)

        top.pack(padx=8, pady=8, anchor="w")


        tk.Label(top, text="Search (symbol / name / atomic #):").pack(side="left")

        self.search_var = tk.StringVar()

        search_entry = tk.Entry(top, textvariable=self.search_var)

        search_entry.pack(side="left", padx=4)

        search_entry.bind("<Return>", lambda e: self.do_search())

        tk.Button(top, text="Search", command=self.do_search).pack(side="left", padx=4)

        tk.Button(top, text="Open elements.json", command=self.open_data_file).pack(side="left", padx=6)


        # Table frame

        table_frame = tk.Frame(self.root)

        table_frame.pack(padx=8, pady=8)


        # create empty grid

        for r in range(1, 8):  # periods 1..7

            for c in range(1, GRID_COLUMNS + 1):

                placeholder = tk.Frame(table_frame, width=46, height=36, bd=0)

                placeholder.grid(row=r, column=c, padx=1, pady=1)


        # Place element buttons based on group/period columns

        for sym, info in self.elements.items():

            group = info.get("group")

            period = info.get("period")

            if group is None or period is None:

                # skip elements without position (lanthanides/actinides not positioned)

                continue

            r = period

            c = group

            btn = tk.Button(table_frame, text=f"{sym}\n{info.get('atomic_number','')}",

                            width=6, height=3, command=partial(self.show_element_popup, sym))

            btn.grid(row=r, column=c, padx=1, pady=1)

            self.buttons[sym] = btn


        # Add legend / info area

        info_frame = tk.Frame(self.root)

        info_frame.pack(fill="x", padx=8, pady=6)

        tk.Label(info_frame, text="Click an element to view details and 3D shell visualization.", fg="gray").pack(side="left")


    def open_data_file(self):

        path = os.path.abspath(DATA_FILE)

        if os.path.exists(path):

            webbrowser.open(f"file://{path}")

        else:

            messagebox.showinfo("Info", f"{DATA_FILE} not found.")


    def do_search(self):

        q = self.search_var.get().strip().lower()

        if not q:

            return

        # search by symbol

        sym_match = None

        for sym, info in self.elements.items():

            if sym.lower() == q:

                sym_match = sym

                break

        if sym_match:

            self.flash_button(sym_match)

            self.show_element_popup(sym_match)

            return

        # search by name

        for sym, info in self.elements.items():

            if info.get("name", "").lower() == q:

                self.flash_button(sym)

                self.show_element_popup(sym)

                return

        # search by atomic number

        if q.isdigit():

            an = int(q)

            sym = self.by_atomic.get(an)

            if sym:

                self.flash_button(sym)

                self.show_element_popup(sym)

                return

        # partial name match

        for sym, info in self.elements.items():

            if q in info.get("name", "").lower():

                self.flash_button(sym)

                self.show_element_popup(sym)

                return

        messagebox.showinfo("Search", "No matching element found.")


    def flash_button(self, sym):

        btn = self.buttons.get(sym)

        if not btn:

            return

        orig = btn.cget("bg")

        def _flash():

            for _ in range(4):

                btn.config(bg="yellow")

                btn.update()

                self.root.after(200)

                btn.config(bg=orig)

                btn.update()

                self.root.after(200)

        threading.Thread(target=_flash, daemon=True).start()


    def show_element_popup(self, sym):

        info = self.elements.get(sym)

        if not info:

            messagebox.showerror("Error", "Element data not found.")

            return

        popup = tk.Toplevel(self.root)

        popup.title(f"{sym} - {info.get('name','')}")

        popup.geometry("420x320")


        left = tk.Frame(popup)

        left.pack(side="left", fill="both", expand=True, padx=8, pady=8)


        tk.Label(left, text=f"{info.get('name','')} ({sym})", font=("Arial", 16, "bold")).pack(anchor="w")

        tk.Label(left, text=f"Atomic Number: {info.get('atomic_number','')}", font=("Arial", 12)).pack(anchor="w")

        tk.Label(left, text=f"Atomic Mass: {info.get('atomic_mass','')}", font=("Arial", 12)).pack(anchor="w")

        tk.Label(left, text=f"Category: {info.get('category','')}", font=("Arial", 12)).pack(anchor="w")

        tk.Label(left, text=f"Electron Configuration: {info.get('electron_configuration','')}", font=("Arial", 11)).pack(anchor="w", pady=6)

        uses = info.get("uses", [])

        tk.Label(left, text="Uses / Applications:", font=("Arial", 12, "underline")).pack(anchor="w", pady=(6,0))

        uses_text = tk.Text(left, height=6, wrap="word")

        uses_text.pack(fill="both", expand=True)

        uses_text.insert("1.0", "\n".join(uses) if isinstance(uses, list) else str(uses))

        uses_text.config(state="disabled")


        # right side controls

        right = tk.Frame(popup)

        right.pack(side="right", fill="y", padx=6, pady=6)

        tk.Button(right, text="Visualize Shells (3D)", command=lambda: threading.Thread(target=show_orbital_visual, args=(info,), daemon=True).start()).pack(pady=6)

        tk.Button(right, text="Close", command=popup.destroy).pack(pady=6)


# ----------------- Run App -----------------

def main():

    root = tk.Tk()

    app = PeriodicTableApp(root)

    root.mainloop()


if __name__ == "__main__":

    main()


Smart Receipt Scanner & Expense Categorizer

import cv2

import pytesseract

import pandas as pd

import matplotlib.pyplot as plt

import re

import os


# Set tesseract path if needed (Windows users)

# pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"


# Predefined keywords for categorization

CATEGORY_KEYWORDS = {

    "Food": ["restaurant", "cafe", "pizza", "burger", "coffee", "food", "dine"],

    "Travel": ["uber", "ola", "taxi", "flight", "airlines", "train", "bus", "travel"],

    "Utilities": ["electricity", "water", "gas", "internet", "wifi", "bill", "utility"],

    "Shopping": ["mall", "store", "supermarket", "shopping", "market", "groceries"],

    "Other": []

}


def preprocess_image(image_path):

    """Convert image to grayscale and apply threshold for better OCR results."""

    img = cv2.imread(image_path)

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    gray = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    return gray


def extract_text(image):

    """Extract text from the image using pytesseract."""

    return pytesseract.image_to_string(image)


def categorize_expense(text):

    """Categorize based on keywords found in the text."""

    text_lower = text.lower()

    for category, keywords in CATEGORY_KEYWORDS.items():

        for word in keywords:

            if word in text_lower:

                return category

    return "Other"


def extract_amount(text):

    """Find the largest number in text assuming it's the total amount."""

    amounts = re.findall(r"\d+\.\d{2}", text)

    if amounts:

        return max(map(float, amounts))

    return 0.0


def process_receipts(folder_path):

    """Process all receipt images in a folder."""

    records = []

    for file in os.listdir(folder_path):

        if file.lower().endswith((".png", ".jpg", ".jpeg")):

            img_path = os.path.join(folder_path, file)

            pre_img = preprocess_image(img_path)

            text = extract_text(pre_img)

            category = categorize_expense(text)

            amount = extract_amount(text)

            records.append({"File": file, "Category": category, "Amount": amount, "Text": text})

    return pd.DataFrame(records)


def plot_expenses(df):

    """Plot expenses by category."""

    category_totals = df.groupby("Category")["Amount"].sum()

    category_totals.plot(kind="bar", color="skyblue")

    plt.title("Expenses by Category")

    plt.xlabel("Category")

    plt.ylabel("Total Amount")

    plt.xticks(rotation=45)

    plt.tight_layout()

    plt.show()


if __name__ == "__main__":

    folder = "receipts"  # Folder containing receipt images

    df = process_receipts(folder)

    print(df)

    plot_expenses(df)


Voice-Controlled Desktop Assistant

import speech_recognition as sr

import pyttsx3

import subprocess

import os

import webbrowser

import time

from datetime import datetime

import threading


# -------------------- Configuration --------------------

# Change these to match your environment

MUSIC_FOLDER = os.path.expanduser("~/Music")  # folder to play music from

SEARCH_ROOT = os.path.expanduser("~")         # root folder for file searches

DEFAULT_BROWSER = None                        # None will use webbrowser.open

# Map simple app names to commands (Windows, macOS, Linux)

COMMANDS = {

    "notepad": {"win": "notepad"},

    "calculator": {"win": "calc", "mac": "open -a Calculator", "linux": "gnome-calculator"},

    "vscode": {"win": r"C:\Users\%USERNAME%\AppData\Local\Programs\Microsoft VS Code\Code.exe",

               "mac": "open -a Visual\\ Studio\\ Code", "linux": "code"},

    "explorer": {"win": "explorer", "mac": "open", "linux": "xdg-open"},

}


# -------------------- Helpers --------------------

def speak(text, engine):

    """Speak text asynchronously so we don't block the main loop."""

    def _s():

        engine.say(text)

        engine.runAndWait()

    t = threading.Thread(target=_s, daemon=True)

    t.start()


def recognize_speech_from_mic(recognizer, microphone, timeout=5, phrase_time_limit=6):

    """Capture audio and return recognized text (or None)."""

    with microphone as source:

        recognizer.adjust_for_ambient_noise(source, duration=0.6)

        try:

            audio = recognizer.listen(source, timeout=timeout, phrase_time_limit=phrase_time_limit)

        except sr.WaitTimeoutError:

            return None

    try:

        # Using Google Web Speech API (requires internet) — good accuracy

        return recognizer.recognize_google(audio)

    except sr.RequestError:

        # API unreachable

        return "[error_api]"

    except sr.UnknownValueError:

        return None


def open_app(app_key):

    """Open an application based on COMMANDS map."""

    platform = os.name  # 'nt' on Windows, 'posix' on macOS/Linux

    is_windows = platform == "nt"

    cmd_map = COMMANDS.get(app_key.lower())

    if not cmd_map:

        return False, f"No mapping for app '{app_key}'."


    try:

        if is_windows:

            cmd = cmd_map.get("win")

            if not cmd:

                return False, "No Windows command available for this app."

            # Support environment vars in path

            cmd = os.path.expandvars(cmd)

            subprocess.Popen(cmd, shell=True)

        else:

            # macOS or Linux (posix)

            cmd = cmd_map.get("mac") or cmd_map.get("linux")

            if not cmd:

                return False, "No command available for this platform."

            subprocess.Popen(cmd, shell=True)

        return True, f"Opened {app_key}."

    except Exception as e:

        return False, str(e)


def open_folder(folder_name):

    """Open a common folder name like downloads, documents, desktop"""

    name = folder_name.strip().lower()

    user = os.path.expanduser("~")

    mapping = {

        "downloads": os.path.join(user, "Downloads"),

        "documents": os.path.join(user, "Documents"),

        "desktop": os.path.join(user, "Desktop"),

        "pictures": os.path.join(user, "Pictures"),

        "music": os.path.join(user, "Music"),

    }

    path = mapping.get(name) or os.path.join(user, name)

    if os.path.exists(path):

        if os.name == "nt":

            os.startfile(path)

        else:

            subprocess.Popen(f'xdg-open "{path}"', shell=True)

        return True, f"Opened folder {path}"

    return False, f"Folder {path} does not exist."


def search_files(query, root=SEARCH_ROOT, limit=10):

    """Simple filename search returning up to `limit` results."""

    results = []

    q = query.lower()

    for dirpath, dirs, files in os.walk(root):

        for f in files:

            if q in f.lower():

                results.append(os.path.join(dirpath, f))

                if len(results) >= limit:

                    return results

    return results


def play_random_music():

    """Play a random music file from MUSIC_FOLDER (if available)."""

    if not os.path.exists(MUSIC_FOLDER):

        return False, f"Music folder {MUSIC_FOLDER} not found."

    exts = (".mp3", ".wav", ".ogg", ".flac")

    files = [f for f in os.listdir(MUSIC_FOLDER) if f.lower().endswith(exts)]

    if not files:

        return False, "No music files found in your music folder."

    choice = os.path.join(MUSIC_FOLDER, files[0])  # pick the first for now

    try:

        if os.name == "nt":

            os.startfile(choice)

        else:

            subprocess.Popen(f'xdg-open "{choice}"', shell=True)

        return True, f"Playing {os.path.basename(choice)}"

    except Exception as e:

        return False, str(e)


def confirm_action(recognizer, microphone, engine, prompt="Are you sure? Say 'yes' to confirm"):

    speak(prompt, engine)

    text = recognize_speech_from_mic(recognizer, microphone, timeout=5, phrase_time_limit=4)

    if text:

        txt = text.lower()

        return "yes" in txt or "yeah" in txt or "yup" in txt

    return False


# -------------------- Main Assistant --------------------

def main():

    # Initialize recognizer and TTS

    recognizer = sr.Recognizer()

    microphone = sr.Microphone()

    engine = pyttsx3.init()

    engine.setProperty('rate', 160)


    speak("Hello — voice assistant activated. Say a command.", engine)

    print("Assistant is listening... (say 'help' for suggestions)")


    while True:

        print("\nListening...")

        text = recognize_speech_from_mic(recognizer, microphone)

        if text is None:

            print("No speech detected.")

            continue

        if text == "[error_api]":

            print("Speech API not reachable. Check internet or use offline recognizer.")

            speak("Sorry, speech service is unreachable.", engine)

            continue


        command = text.lower().strip()

        print("You said:", command)


        # Exit

        if any(w in command for w in ("exit", "quit", "stop", "shutdown assistant", "bye")):

            speak("Goodbye!", engine)

            print("Exiting assistant.")

            break


        # Help

        if "help" in command:

            help_text = ("You can say: open notepad, open calculator, open downloads, search file invoice, "

                         "play music, open website youtube, what's the time, shutdown system (requires confirmation).")

            speak(help_text, engine)

            print(help_text)

            continue


        # Time

        if "time" in command:

            now = datetime.now().strftime("%I:%M %p")

            speak(f"The time is {now}", engine)

            print("Time:", now)

            continue


        # Open website

        if command.startswith("open website") or command.startswith("open url") or command.startswith("open "):

            # e.g. "open website youtube" or "open youtube"

            parts = command.replace("open website", "").replace("open url", "").replace("open ", "").strip()

            site = parts.split()[0] if parts else None

            if site:

                url = site if site.startswith("http") else f"https://{site}.com"

                webbrowser.open(url)

                speak(f"Opening {site}", engine)

                print("Open URL:", url)

                continue


        # Open app mapping

        if command.startswith("open ") and not command.startswith("open website"):

            # e.g. "open notepad" or "open vscode"

            app = command.replace("open ", "").strip()

            success, msg = open_app(app)

            speak(msg if msg else ("Opened " + app), engine)

            print(msg)

            continue


        # Open folder

        if command.startswith("open folder") or command.startswith("open downloads") or command.startswith("open desktop"):

            folder_name = command.replace("open folder", "").replace("open ", "").strip()

            success, msg = open_folder(folder_name or "downloads")

            speak(msg, engine)

            print(msg)

            continue


        # Search files

        if command.startswith("search file") or command.startswith("find file") or command.startswith("search for file"):

            # e.g. "search file invoice 2024"

            query = command.split("file", 1)[-1].strip()

            if not query:

                speak("Please say the file name to search for.", engine)

                continue

            speak(f"Searching for files that match {query}", engine)

            print("Searching for:", query)

            results = search_files(query)

            if not results:

                speak("No files found.", engine)

                print("No results.")

            else:

                speak(f"I found {len(results)} files. First one is {os.path.basename(results[0])}", engine)

                print("\n".join(results[:10]))

            continue


        # Play music

        if "music" in command or command.startswith("play music"):

            ok, msg = play_random_music()

            speak(msg, engine) if ok else speak("Could not play music.", engine)

            print(msg)

            continue


        # Shutdown system (dangerous) — confirm first

        if "shutdown" in command or "restart system" in command:

            speak("You asked to perform a system operation. This requires confirmation.", engine)

            confirmed = confirm_action(recognizer, microphone, engine,

                                       prompt="Please say yes to confirm shutting down the system.")

            if confirmed:

                speak("Performing the operation now.", engine)

                print("Confirmed. Executing shutdown/restart.")

                if "shutdown" in command:

                    if os.name == "nt":

                        subprocess.Popen("shutdown /s /t 5", shell=True)

                    else:

                        subprocess.Popen("sudo shutdown -h now", shell=True)

                else:

                    # restart

                    if os.name == "nt":

                        subprocess.Popen("shutdown /r /t 5", shell=True)

                    else:

                        subprocess.Popen("sudo reboot", shell=True)

            else:

                speak("Operation cancelled.", engine)

            continue


        # Search web (fallback)

        if command.startswith("search ") or command.startswith("google "):

            query = command.replace("search", "").replace("google", "").strip()

            url = f"https://www.google.com/search?q={query.replace(' ', '+')}"

            webbrowser.open(url)

            speak(f"Searching the web for {query}", engine)

            print("Web search:", query)

            continue


        # Unknown command fallback

        speak("I did not understand that. Say help for suggestions.", engine)

        print("Unhandled command.")


if __name__ == "__main__":

    try:

        main()

    except KeyboardInterrupt:

        print("\nAssistant stopped by user.")

Virtual Phone Dialer

import tkinter as tk

from tkinter import messagebox

import sqlite3

from datetime import datetime


# ------------------ DATABASE ------------------ #


conn = sqlite3.connect('call_history.db')

cursor = conn.cursor()

cursor.execute('''

    CREATE TABLE IF NOT EXISTS calls (

        id INTEGER PRIMARY KEY AUTOINCREMENT,

        number TEXT NOT NULL,

        timestamp TEXT NOT NULL

    )

''')

conn.commit()



# ------------------ MAIN DIALER APP ------------------ #


class PhoneDialer:

    def __init__(self, root):

        self.root = root

        self.root.title("📱 Virtual Phone Dialer")

        self.number_var = tk.StringVar()


        self.create_widgets()


    def create_widgets(self):

        # Entry to show the number

        self.display = tk.Entry(self.root, textvariable=self.number_var, font=('Helvetica', 20), bd=10, justify='right')

        self.display.grid(row=0, column=0, columnspan=3, pady=10)


        # Keypad buttons

        btn_texts = [

            '1', '2', '3',

            '4', '5', '6',

            '7', '8', '9',

            '*', '0', '#'

        ]


        r = 1

        c = 0

        for text in btn_texts:

            tk.Button(self.root, text=text, font=('Helvetica', 18), width=5, height=2,

                      command=lambda t=text: self.append_number(t)).grid(row=r, column=c, padx=5, pady=5)

            c += 1

            if c > 2:

                c = 0

                r += 1


        # Call button

        tk.Button(self.root, text="📞 Call", bg="green", fg="white", font=('Helvetica', 18),

                  command=self.make_call).grid(row=5, column=0, pady=10, padx=5, sticky='we')


        # Clear button

        tk.Button(self.root, text="❌ Clear", bg="orange", fg="black", font=('Helvetica', 18),

                  command=self.clear).grid(row=5, column=1, pady=10, padx=5, sticky='we')


        # History button

        tk.Button(self.root, text="🕒 History", bg="blue", fg="white", font=('Helvetica', 18),

                  command=self.show_history).grid(row=5, column=2, pady=10, padx=5, sticky='we')


    def append_number(self, val):

        current = self.number_var.get()

        self.number_var.set(current + val)


    def clear(self):

        self.number_var.set("")


    def make_call(self):

        number = self.number_var.get()

        if number:

            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            cursor.execute("INSERT INTO calls (number, timestamp) VALUES (?, ?)", (number, timestamp))

            conn.commit()

            messagebox.showinfo("Calling", f"Calling {number}...\n(Call logged)")

            self.clear()

        else:

            messagebox.showwarning("Empty", "Please enter a number")


    def show_history(self):

        history_win = tk.Toplevel(self.root)

        history_win.title("📜 Call History")


        tk.Label(history_win, text="Number", font=('Helvetica', 14, 'bold')).grid(row=0, column=0, padx=10, pady=5)

        tk.Label(history_win, text="Timestamp", font=('Helvetica', 14, 'bold')).grid(row=0, column=1, padx=10, pady=5)


        cursor.execute("SELECT number, timestamp FROM calls ORDER BY id DESC")

        rows = cursor.fetchall()


        for idx, (number, timestamp) in enumerate(rows):

            tk.Label(history_win, text=number, font=('Helvetica', 12)).grid(row=idx+1, column=0, padx=10, sticky='w')

            tk.Label(history_win, text=timestamp, font=('Helvetica', 12)).grid(row=idx+1, column=1, padx=10, sticky='w')



# ------------------ RUN APP ------------------ #


if __name__ == "__main__":

    root = tk.Tk()

    app = PhoneDialer(root)

    root.mainloop()