import pygame import pyttsx3 import threading import queue import random import os import requests import json import datetime # ========================================================= # KID'S LEARNING APP – v4 # ✅ Typing Mode # ✅ Alphabet Mode with REAL photos # ✅ Auto-download & cache images locally # ✅ Mouse cursor visible & confined to the window # ✅ Only F12 can exit — Alt+F4 / accidental close disabled # ✅ Multi-source image fallback (Wikipedia API, Wikipedia REST, Commons search) # ✅ No-repeat word cycling per letter # ✅ Voice Support # ✅ Fullscreen # ========================================================= # --------------------------------------------------------- # Real photos fetched from Wikipedia's free public API. # No API key needed. Works on any Windows/Mac/Linux machine. # Each word maps to the best Wikipedia article title that # gives a clear, recognisable photo of that object. # --------------------------------------------------------- # word → Wikipedia article title (used to fetch thumbnail) WIKI_TITLES = { # ── A ── "Apple" : "Apple", "Ant" : "Ant", "Aeroplane" : "Airplane", "Alligator" : "American alligator", "Astronaut" : "Astronaut", "Anchor" : "Anchor", "Angel" : "Angel", "Apricot" : "Apricot", "Arm" : "Arm", "Arrow" : "Arrow", "Avocado" : "Avocado", "Acorn" : "Acorn", "Ambulance" : "Ambulance", "Axe" : "Axe", "Ankle" : "Ankle", "Album" : "Photo album", "Artist" : "Artist", "Apron" : "Apron", "Aquarium" : "Aquarium", "Alarm Clock" : "Alarm clock", # ── B ── "Ball" : "Ball", "Banana" : "Banana", "Butterfly" : "Butterfly", "Bear" : "Brown bear", "Boat" : "Boat", "Bird" : "Bird", "Bee" : "Bee", "Bus" : "Bus", "Balloon" : "Balloon", "Book" : "Book", "Bottle" : "Bottle", "Bread" : "Bread", "Bridge" : "Bridge", "Brush" : "Toothbrush", "Baby" : "Baby", "Basket" : "Basket", "Bell" : "Bell", "Bed" : "Bed", "Bicycle" : "Bicycle", "Bucket" : "Bucket", # ── C ── "Cat" : "Cat", "Car" : "Car", "Cow" : "Cattle", "Cake" : "Cake", "Castle" : "Castle", "Castanets" : "Castanets", "Camel" : "Camel", "Candle" : "Candle", "Cap" : "Cap", "Carrot" : "Carrot", "Chair" : "Chair", "Cheese" : "Cheese", "Cherry" : "Cherry", "Chicken" : "Chicken", "Clock" : "Clock", "Cloud" : "Cloud", "Coconut" : "Coconut", "Cookie" : "Cookie", "Cup" : "Cup", "Crayon" : "Crayon", # ── D ── "Dog" : "Dog", "Duck" : "Duck", "Drum" : "Drum", "Dolphin" : "Dolphin", "Dragon" : "Komodo dragon", "Deer" : "Deer", "Donkey" : "Donkey", "Door" : "Door", "Dinosaur" : "Dinosaur", "Diamond" : "Diamond", "Desk" : "Desk", "Daisy" : "Daisy", "Dice" : "Dice", "Dress" : "Dress", "Doctor" : "Doctor", "Dish" : "Dish", "Donut" : "Doughnut", "Dragonfly" : "Dragonfly", "Drill" : "Hand drill", "Drinking Glass": "Drinking glass", # ── E ── "Elephant" : "Elephant", "Eagle" : "Eagle", "Earth" : "Earth", "Egg" : "Egg", "Engine" : "Steam locomotive", "Ear" : "Ear", "Envelope" : "Envelope", "Elf" : "Elf", "Escalator" : "Escalator", "Eye" : "Eye", "Easel" : "Easel", "Eraser" : "Eraser", "Excavator" : "Excavator", "Eggplant" : "Eggplant", "Elbow" : "Elbow", "Eel" : "Eel", "Electric Fan" : "Electric fan", "Earthworm" : "Earthworm", "Engineer" : "Engineer", "Exit" : "Exit sign", # ── F ── "Fish" : "Fish", "Frog" : "Frog", "Flower" : "Flower", "Fox" : "Fox", "Flag" : "Flag", "Fan" : "Electric fan", "Farm" : "Farm", "Feather" : "Feather", "Fence" : "Fence", "Finger" : "Finger", "Fire" : "Fire", "Fire Truck" : "Fire engine", "Firefighter" : "Firefighter", "Flamingo" : "Flamingo", "Flashlight" : "Flashlight", "Flip Flops" : "Flip-flops", "Floor" : "Floor", "Flute" : "Flute", "Football" : "Football", "Fork" : "Fork", # ── G ── "Goat" : "Goat", "Grapes" : "Grape", "Giraffe" : "Giraffe", "Guitar" : "Guitar", "Garden" : "Garden", "Gift" : "Gift", "Girl" : "Girl", "Glass" : "Glass", "Glasses" : "Eyeglasses", "Globe" : "Globe", "Gloves" : "Gloves", "Goose" : "Goose", "Gorilla" : "Gorilla", "Grass" : "Grass", "Green Peas" : "Green peas", "Gate" : "Gate", "Goldfish" : "Goldfish", "Garbage Truck" : "Garbage truck", "Garlic" : "Garlic", "Gift Box" : "Gift box", # ── H ── "Horse" : "Horse", "Hat" : "Hat", "House" : "House", "Honey" : "Honey", "Helicopter" : "Helicopter", "Hammer" : "Hammer", "Hamburger" : "Hamburger", "Hamster" : "Hamster", "Hand" : "Hand", "Hanger" : "Clothes hanger", "Happy Face" : "Smiley face", "Harp" : "Harp", "Heart" : "Heart", "Hen" : "Hen", "Hippopotamus" : "Hippopotamus", "Hole" : "Hole", "Hospital" : "Hospital", "Hot Air Balloon": "Hot air balloon", "Hot Dog" : "Hot dog", "Helmet" : "Helmet", # ── I ── "Ice Cream" : "Ice cream", "Igloo" : "Igloo", "Insect" : "Insect", "Island" : "Island", "Iron" : "Clothes iron", "Ice Cube" : "Ice cube", "Iceberg" : "Iceberg", "Ink" : "Ink", "Ink Bottle" : "Ink bottle", "Invitation" : "Invitation card", "Internet" : "Internet", "Ivy" : "Ivy plant", "Icicle" : "Icicle", "Ice Skate" : "Ice skate", "Instrument" : "Musical instrument", "Incubator" : "Incubator", "Ice Tea" : "Iced tea", "Indian Flag" : "Flag of India", "Iguana" : "Green iguana", "Id Card" : "Identification card", # ── J ── "Jaguar" : "Jaguar", "Jellyfish" : "Jellyfish", "Jar" : "Jar", "Jet" : "Jet aircraft", "Jungle" : "Jungle", "Jam" : "Jam", "Jeans" : "Jeans", "Jeep" : "Jeep", "Jewel" : "Jewel", "Juice" : "Juice", "Juicer" : "Juicer", "Jacket" : "Jacket", "Jigsaw Puzzle" : "Jigsaw puzzle", "Joystick" : "Joystick", "Jug" : "Jug", "Jump Rope" : "Jump rope", "Juggler" : "Juggler", "Jelly" : "Jelly", "Jackfruit" : "Jackfruit", "Jet Ski" : "Jet ski", # ── K ── "Kite" : "Kite", "Kangaroo" : "Kangaroo", "Key" : "Key (lock)", "King" : "King", "Kitchen" : "Kitchen", "Koala" : "Koala", "Kiwi" : "Kiwi fruit", "Kitten" : "Kitten", "Keyboard" : "Computer keyboard", "Kettle" : "Kettle", "Knife" : "Knife", "Knee" : "Knee", "Knapsack" : "Backpack", "Kayak" : "Kayak", "Kid" : "Child", "Ketchup" : "Ketchup", "Kernel" : "Corn kernel", "Kitchen Spoon" : "Wooden spoon", "Kettle Drum" : "Kettledrum", "Kiosk" : "Kiosk", # ── L ── "Lion" : "Lion", "Lemon" : "Lemon", "Leaf" : "Leaf", "Ladder" : "Ladder", "Lighthouse" : "Lighthouse", "Lamp" : "Lamp", "Ladybug" : "Ladybug", "Lake" : "Lake", "Lamb" : "Lamb", "Lantern" : "Lantern", "Leafy Plant" : "Leafy plant", "Leg" : "Leg", "Lettuce" : "Lettuce", "Lollipop" : "Lollipop", "Lock" : "Padlock", "Lobster" : "Lobster", "Lunch Box" : "Lunch box", "Lizard" : "Lizard", "Log" : "Wood log", "Library" : "Library", "Lotus" : "Lotus", # ── M ── "Monkey" : "Monkey", "Moon" : "Moon", "Mango" : "Mango", "Mouse" : "Mouse", "Mountain" : "Mountain", "Milk" : "Milk", "Mirror" : "Mirror", "Mug" : "Mug", "Motorcycle" : "Motorcycle", "Map" : "Map", "Marker" : "Marker pen", "Mask" : "Mask", "Mat" : "Mat", "Medicine" : "Medicine", "Microscope" : "Microscope", "Milkshake" : "Milkshake", "Mitten" : "Mitten", "Mushroom" : "Mushroom", "Music" : "Music", "Magnet" : "Magnet", # ── N ── "Nest" : "Bird nest", "Nurse" : "Nurse", "Needle" : "Sewing needle", "Nightingale" : "Common nightingale", "Newspaper" : "Newspaper", "Nose" : "Nose", "Notebook" : "Notebook", "Necklace" : "Necklace", "Napkin" : "Napkin", "Net" : "Net", "Nail" : "Nail", "Nail Cutter" : "Nail clipper", "Noodles" : "Noodles", "Nut" : "Nut", "Number" : "Number", "Nestling" : "Baby bird", "Night" : "Night sky", "North" : "North direction", "Notebook Bag" : "School bag", "Nature" : "Nature", # ── O ── "Orange" : "Orange (fruit)", "Owl" : "Owl", "Ocean" : "Ocean", "Ostrich" : "Ostrich", "Octopus" : "Octopus", "Onion" : "Onion", "Olive" : "Olive", "Oven" : "Oven", "Ox" : "Ox", "Orchid" : "Orchid", "Oar" : "Oar", "Office" : "Office", "Oil Lamp" : "Oil lamp", "Open Book" : "Open book", "Opera House" : "Opera house", "Omelette" : "Omelette", "Ornament" : "Ornament", "Outlet" : "Electrical outlet", "Otter" : "Otter", "Organ" : "Pipe organ", # ── P ── "Parrot" : "Parrot", "Panda" : "Giant panda", "Penguin" : "Penguin", "Pizza" : "Pizza", "Pineapple" : "Pineapple", "Pig" : "Pig", "Peacock" : "Peacock", "Pear" : "Pear", "Peach" : "Peach", "Pencil" : "Pencil", "Pen" : "Pen", "Paper" : "Paper", "Pumpkin" : "Pumpkin", "Potato" : "Potato", "Police Car" : "Police car", "Police Officer": "Police officer", "Puppet" : "Hand puppet", "Purse" : "Purse", "Plant" : "Plant", "Pillow" : "Pillow", # ── Q ── "Queen" : "Queen", "Quail" : "Quail", "Quilt" : "Quilt", "Question" : "Question mark", "Quarter" : "Quarter coin", "Quiet" : "Quiet sign", "Quiver" : "Quiver", "Quartz" : "Quartz", "Quokka" : "Quokka", "Quiche" : "Quiche", "Quicksand" : "Quicksand", "Quill" : "Feather quill", "Quotation" : "Quotation marks", "Quad Bike" : "All-terrain vehicle", "Quadcopter" : "Quadcopter drone", "Queen Bee" : "Queen bee", "Queen Crown" : "Royal crown", "Quiz" : "Quiz", "Quince" : "Quince", "Queue" : "Queue", # ── R ── "Rabbit" : "Rabbit", "Rose" : "Rose", "Rainbow" : "Rainbow", "Rocket" : "Rocket", "Ring" : "Ring (jewellery)", "Robot" : "Robot", "Rain" : "Rain", "Raincoat" : "Raincoat", "Rain Boots" : "Rain boots", "Ruler" : "Ruler", "Radio" : "Radio", "Raspberry" : "Raspberry", "Road" : "Road", "Rock" : "Rock", "Rooster" : "Rooster", "Ribbon" : "Ribbon", "River" : "River", "Roller Skates" : "Roller skates", "Room" : "Room", "Rope" : "Rope", # ── S ── "Sun" : "Sun", "Star" : "Star", "Snake" : "Snake", "Ship" : "Ship", "Strawberry" : "Strawberry", "School" : "School", "Scissors" : "Scissors", "Scooter" : "Scooter", "Seahorse" : "Seahorse", "Seal" : "Seal", "Shark" : "Shark", "Sheep" : "Sheep", "Shell" : "Seashell", "Shoe" : "Shoe", "Snowman" : "Snowman", "Soap" : "Soap", "Socks" : "Socks", "Spider" : "Spider", "Spoon" : "Spoon", "Swing" : "Swing", # ── T ── "Tiger" : "Tiger", "Tree" : "Tree", "Train" : "Train", "Turtle" : "Turtle", "Tomato" : "Tomato", "Table" : "Table", "Taxi" : "Taxi", "Teacher" : "Teacher", "Teddy Bear" : "Teddy bear", "Telephone" : "Telephone", "Television" : "Television", "Tent" : "Tent", "Thumb" : "Thumb", "Toast" : "Toast", "Tooth" : "Tooth", "Toothbrush" : "Toothbrush", "Tractor" : "Tractor", "Traffic Light" : "Traffic light", "Truck" : "Truck", "Trumpet" : "Trumpet", # ── U ── "Umbrella" : "Umbrella", "Unicorn" : "Unicorn", "Universe" : "Universe", "Ukulele" : "Ukulele", "Uniform" : "Uniform", "Unicycle" : "Unicycle", "Utensils" : "Kitchen utensils", "Urn" : "Urn", "USB Drive" : "USB flash drive", "Umpire" : "Umpire", "Up Arrow" : "Up arrow", "Upside Down" : "Upside down", "Utility Truck" : "Utility truck", "Urchin" : "Sea urchin", "Udon" : "Udon noodles", "Utility Pole" : "Utility pole", "Ultrasound" : "Ultrasound machine", "Ukulele Case" : "Ukulele case", "Underpass" : "Underpass", "United Flag" : "National flag", # ── V ── "Van" : "Van", "Violin" : "Violin", "Volcano" : "Volcano", "Vulture" : "Vulture", "Vegetables" : "Vegetable", "Vase" : "Vase", "Vegetable Soup": "Vegetable soup", "Vest" : "Vest", "Vacuum Cleaner": "Vacuum cleaner", "Village" : "Village", "Vine" : "Vine", "Video Camera" : "Video camera", "Visitor" : "Visitor", "Velvet" : "Velvet", "Volleyball" : "Volleyball", "Vitamin" : "Vitamin", "Viaduct" : "Viaduct", "Vanilla" : "Vanilla", "Vet" : "Veterinarian", "Village House" : "Village house", # ── W ── "Whale" : "Whale", "Wolf" : "Wolf", "Watermelon" : "Watermelon", "Watch" : "Watch", "Windmill" : "Windmill", "Water" : "Water", "Window" : "Window", "Wheel" : "Wheel", "Whistle" : "Whistle", "Wagon" : "Wagon", "Wallet" : "Wallet", "Washing Machine": "Washing machine", "Waterfall" : "Waterfall", "Water Bottle" : "Water bottle", "Well" : "Water well", "Wheelbarrow" : "Wheelbarrow", "Wheat" : "Wheat", "Worm" : "Earthworm", "Wreath" : "Wreath", "Woodpecker" : "Woodpecker", # ── X ── "Xylophone" : "Xylophone", "X-ray" : "X-ray", "Xerox" : "Xerox", "Xmas Tree" : "Christmas tree", "Xenon" : "Xenon", "X-ray Fish" : "X-ray tetra", "Xylitol" : "Xylitol", "Xenops" : "Xenops bird", "Xenopus" : "African clawed frog", "Xylocarp" : "Xylocarp", "Xyst" : "Garden walkway", "Xenarthra" : "Armadillo", "Xylophone Stick": "Mallet", "Xmas Bell" : "Christmas bell", "Xmas Gift" : "Christmas gift", "Xmas Stocking" : "Christmas stocking", "Xmas Star" : "Christmas star", "Xmas Wreath" : "Christmas wreath", "Xenolith" : "Rock fragment", "Xylite" : "Brown coal", # ── Y ── "Yak" : "Yak", "Yacht" : "Yacht", "Yo-Yo" : "Yo-yo", "Yarn" : "Yarn", "Yogurt" : "Yogurt", "Yam" : "Yam", "Yellow" : "Yellow color", "Yellow Bird" : "Canary", "Yolk" : "Egg yolk", "Yummy Cake" : "Cake", "Yeti" : "Yeti", "Yard" : "Yard", "Yoga" : "Yoga", "Yogurt Cup" : "Yogurt cup", "Young Lion" : "Lion cub", "Young Duck" : "Duckling", "Young Goat" : "Kid goat", "Yellow Flower" : "Yellow flower", "Yellow Bus" : "School bus", "Yew Tree" : "Yew tree", # ── Z ── "Zebra" : "Zebra", "Zoo" : "Zoo", "Zucchini" : "Zucchini", "Zip" : "Zipper", "Zoo Keeper" : "Zookeeper", "Zeppelin" : "Airship", "Zero" : "Number zero", "Zigzag" : "Zigzag", "Zinnia" : "Zinnia flower", "Zebra Crossing": "Pedestrian crossing", "Zipper Bag" : "Zipper bag", "Zoo Train" : "Zoo train", "Zebra Fish" : "Zebrafish", "Zombie" : "Cartoon zombie", "Zoom" : "Magnifying glass", "Zest" : "Orange zest", "Zen Garden" : "Zen garden", "Zinc" : "Zinc", "Zipline" : "Zip line", "Zorilla" : "Zorilla", } # Letter → list of words ALPHABET_WORDS = { "A": [ "Apple", "Ant", "Aeroplane", "Alligator", "Astronaut", "Anchor", "Angel", "Apricot", "Arm", "Arrow", "Avocado", "Acorn", "Ambulance", "Axe", "Ankle", "Album", "Artist", "Apron", "Aquarium", "Alarm Clock" ], "B": [ "Ball", "Banana", "Butterfly", "Bear", "Boat", "Bird", "Bee", "Bus", "Balloon", "Book", "Bottle", "Bread", "Bridge", "Brush", "Baby", "Basket", "Bell", "Bed", "Bicycle", "Bucket" ], "C": [ "Cat", "Car", "Cow", "Cake", "Castle", "Castanets", "Camel", "Candle", "Cap", "Carrot", "Chair", "Cheese", "Cherry", "Chicken", "Clock", "Cloud", "Coconut", "Cookie", "Cup", "Crayon" ], "D": [ "Dog", "Duck", "Drum", "Dolphin", "Dragon", "Deer", "Donkey", "Door", "Dinosaur", "Diamond", "Desk", "Daisy", "Dice", "Dress", "Doctor", "Dish", "Donut", "Dragonfly", "Drill", "Drinking Glass" ], "E": [ "Elephant", "Eagle", "Earth", "Egg", "Engine", "Ear", "Envelope", "Elf", "Escalator", "Eye", "Easel", "Eraser", "Excavator", "Eggplant", "Elbow", "Eel", "Electric Fan", "Earthworm", "Engineer", "Exit" ], "F": [ "Fish", "Frog", "Flower", "Fox", "Flag", "Fan", "Farm", "Feather", "Fence", "Finger", "Fire", "Fire Truck", "Firefighter", "Flamingo", "Flashlight", "Flip Flops", "Floor", "Flute", "Football", "Fork" ], "G": [ "Goat", "Grapes", "Giraffe", "Guitar", "Garden", "Gift", "Girl", "Glass", "Glasses", "Globe", "Gloves", "Goose", "Gorilla", "Grass", "Green Peas", "Gate", "Goldfish", "Garbage Truck", "Garlic", "Gift Box" ], "H": [ "Horse", "Hat", "House", "Honey", "Helicopter", "Hammer", "Hamburger", "Hamster", "Hand", "Hanger", "Happy Face", "Harp", "Heart", "Hen", "Hippopotamus", "Hole", "Hospital", "Hot Air Balloon", "Hot Dog", "Helmet" ], "I": [ "Ice Cream", "Igloo", "Insect", "Island", "Iron", "Ice Cube", "Iceberg", "Ink", "Ink Bottle", "Invitation", "Internet", "Ivy", "Icicle", "Ice Skate", "Instrument", "Incubator", "Ice Tea", "Indian Flag", "Iguana", "Id Card" ], "J": [ "Jaguar", "Jellyfish", "Jar", "Jet", "Jungle", "Jam", "Jeans", "Jeep", "Jewel", "Juice", "Juicer", "Jacket", "Jigsaw Puzzle", "Joystick", "Jug", "Jump Rope", "Juggler", "Jelly", "Jackfruit", "Jet Ski" ], "K": [ "Kite", "Kangaroo", "Key", "King", "Kitchen", "Koala", "Kiwi", "Kitten", "Keyboard", "Kettle", "Knife", "Knee", "Knapsack", "Kayak", "Kid", "Ketchup", "Kernel", "Kitchen Spoon", "Kettle Drum", "Kiosk" ], "L": [ "Lion", "Lemon", "Leaf", "Ladder", "Lighthouse", "Lotus", "Lamp", "Ladybug", "Lake", "Lamb", "Lantern", "Leafy Plant", "Leg", "Lettuce", "Lollipop", "Lock", "Lobster", "Lunch Box", "Lizard", "Log", "Library" ], "M": [ "Monkey", "Moon", "Mango", "Mouse", "Mountain", "Milk", "Mirror", "Mug", "Motorcycle", "Map", "Marker", "Mask", "Mat", "Medicine", "Microscope", "Milkshake", "Mitten", "Mushroom", "Music", "Magnet" ], "N": [ "Nest", "Nurse", "Needle", "Nightingale", "Newspaper", "Nose", "Notebook", "Necklace", "Napkin", "Net", "Nail", "Nail Cutter", "Noodles", "Nut", "Number", "Nestling", "Night", "North", "Notebook Bag", "Nature" ], "O": [ "Orange", "Owl", "Ocean", "Ostrich", "Octopus", "Onion", "Olive", "Oven", "Ox", "Orchid", "Oar", "Office", "Oil Lamp", "Open Book", "Opera House", "Omelette", "Ornament", "Outlet", "Otter", "Organ" ], "P": [ "Parrot", "Panda", "Penguin", "Pizza", "Pineapple", "Pig", "Peacock", "Pear", "Peach", "Pencil", "Pen", "Paper", "Pumpkin", "Potato", "Police Car", "Police Officer", "Puppet", "Purse", "Plant", "Pillow" ], "Q": [ "Queen", "Quail", "Quilt", "Question", "Quarter", "Quiet", "Quiver", "Quartz", "Quokka", "Quiche", "Quicksand", "Quill", "Quotation", "Quad Bike", "Quadcopter", "Queen Bee", "Queen Crown", "Quiz", "Quince", "Queue" ], "R": [ "Rabbit", "Rose", "Rainbow", "Rocket", "Ring", "Robot", "Rain", "Raincoat", "Rain Boots", "Ruler", "Radio", "Raspberry", "Road", "Rock", "Rooster", "Ribbon", "River", "Roller Skates", "Room", "Rope" ], "S": [ "Sun", "Star", "Snake", "Ship", "Strawberry", "School", "Scissors", "Scooter", "Seahorse", "Seal", "Shark", "Sheep", "Shell", "Shoe", "Snowman", "Soap", "Socks", "Spider", "Spoon", "Swing" ], "T": [ "Tiger", "Tree", "Train", "Turtle", "Tomato", "Table", "Taxi", "Teacher", "Teddy Bear", "Telephone", "Television", "Tent", "Thumb", "Toast", "Tooth", "Toothbrush", "Tractor", "Traffic Light", "Truck", "Trumpet" ], "U": [ "Umbrella", "Unicorn", "Universe", "Ukulele", "Uniform", "Unicycle", "Utensils", "Urn", "USB Drive", "Umpire", "Up Arrow", "Upside Down", "Utility Truck", "Urchin", "Udon", "Utility Pole", "Ultrasound", "Ukulele Case", "Underpass", "United Flag" ], "V": [ "Van", "Violin", "Volcano", "Vulture", "Vegetables", "Vase", "Vegetable Soup", "Vest", "Vacuum Cleaner", "Village", "Vine", "Video Camera", "Visitor", "Velvet", "Volleyball", "Vitamin", "Viaduct", "Vanilla", "Vet", "Village House" ], "W": [ "Whale", "Wolf", "Watermelon", "Watch", "Windmill", "Water", "Window", "Wheel", "Whistle", "Wagon", "Wallet", "Washing Machine", "Waterfall", "Water Bottle", "Well", "Wheelbarrow", "Wheat", "Worm", "Wreath", "Woodpecker" ], "X": [ "Xylophone", "X-ray", "Xerox", "Xmas Tree", "Xenon", "X-ray Fish", "Xylitol", "Xenops", "Xenopus", "Xylocarp", "Xyst", "Xenarthra", "Xylophone Stick", "Xmas Bell", "Xmas Gift", "Xmas Stocking", "Xmas Star", "Xmas Wreath", "Xenolith", "Xylite" ], "Y": [ "Yak", "Yacht", "Yo-Yo", "Yarn", "Yogurt", "Yam", "Yellow", "Yellow Bird", "Yolk", "Yummy Cake", "Yeti", "Yard", "Yoga", "Yogurt Cup", "Young Lion", "Young Duck", "Young Goat", "Yellow Flower", "Yellow Bus", "Yew Tree" ], "Z": [ "Zebra", "Zoo", "Zucchini", "Zip", "Zoo Keeper", "Zeppelin", "Zero", "Zigzag", "Zinnia", "Zebra Crossing", "Zipper Bag", "Zoo Train", "Zebra Fish", "Zombie", "Zoom", "Zest", "Zen Garden", "Zinc", "Zipline", "Zorilla" ], } # --------------------------------------------------------- # MANUAL IMAGE OVERRIDES # If a word's photo keeps failing to auto-download, paste a # direct image URL here (right-click any web image -> "Copy # Image Address") and it will be used instead of searching. # Example: # "Xerox": "https://example.com/some_photo.jpg", # --------------------------------------------------------- MANUAL_IMAGE_URLS = { # "Word": "https://direct-link-to-image.jpg", } # ========================================================= # IMAGE CACHE # ========================================================= CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "abc_images") os.makedirs(CACHE_DIR, exist_ok=True) MISSING_LOG = os.path.join(CACHE_DIR, "missing_images.log") HEADERS = { "User-Agent": "KidsLearningApp/4.0 (educational; contact: parent@home)" } def cache_path(word): safe = word.lower().replace(" ", "_").replace("/", "_").replace("-", "_") return os.path.join(CACHE_DIR, safe + ".jpg") def find_cached_image(word): """Check for any supported image format in cache dir.""" safe = word.lower().replace(" ", "_").replace("/", "_").replace("-", "_") for ext in (".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp"): p = os.path.join(CACHE_DIR, safe + ext) if os.path.exists(p): return p return None def log_missing(word, reason=""): """ Record a word whose image could not be fetched automatically, so the user knows exactly which ones need a manual image drop into the abc_images folder (see cache_path() naming convention). """ try: safe = word.lower().replace(" ", "_").replace("/", "_").replace("-", "_") line = (f"[{datetime.datetime.now().isoformat(timespec='seconds')}] " f"{word} -> expected filename: {safe}.jpg {reason}\n") with open(MISSING_LOG, "a", encoding="utf-8") as f: f.write(line) except Exception: pass # --------------------------------------------------------- # Source 1: Wikipedia "pageimages" action API # --------------------------------------------------------- def _try_wikipedia_pageimages(title): api = ( "https://en.wikipedia.org/w/api.php" "?action=query" f"&titles={requests.utils.quote(title)}" "&prop=pageimages" "&format=json" "&pithumbsize=800" ) try: r = requests.get(api, timeout=10, headers=HEADERS) if r.status_code != 200: return None data = r.json() pages = data.get("query", {}).get("pages", {}) for page in pages.values(): url = page.get("thumbnail", {}).get("source", "") if url: return url except Exception as e: print(f"[WIKI pageimages] {title}: {e}") return None # --------------------------------------------------------- # Source 2: Wikipedia REST "summary" endpoint (different # backend/cache path than the action API above, so it # sometimes succeeds when Source 1 fails or times out) # --------------------------------------------------------- def _try_wikipedia_rest_summary(title): api = f"https://en.wikipedia.org/api/rest_v1/page/summary/{requests.utils.quote(title)}" try: r = requests.get(api, timeout=10, headers=HEADERS) if r.status_code != 200: return None data = r.json() for key in ("originalimage", "thumbnail"): url = data.get(key, {}).get("source", "") if url: return url except Exception as e: print(f"[WIKI rest-summary] {title}: {e}") return None # --------------------------------------------------------- # Source 3: Wikimedia Commons file search. Useful for words # whose Wikipedia article itself has no lead image, or whose # title doesn't exactly match an article. # --------------------------------------------------------- def _try_commons_search(word): api = ( "https://commons.wikimedia.org/w/api.php" "?action=query&generator=search" f"&gsrsearch={requests.utils.quote(word)}" "&gsrnamespace=6&gsrlimit=5" "&prop=imageinfo&iiprop=url&iiurlwidth=800" "&format=json" ) try: r = requests.get(api, timeout=10, headers=HEADERS) if r.status_code != 200: return None data = r.json() pages = data.get("query", {}).get("pages", {}) for page in pages.values(): infos = page.get("imageinfo", []) if infos: url = infos[0].get("thumburl") or infos[0].get("url") if url and url.lower().endswith((".jpg", ".jpeg", ".png")): return url except Exception as e: print(f"[COMMONS search] {word}: {e}") return None def get_image_url(word): """ Try, in order: 0. Manual override URL (if the user supplied one) 1. Wikipedia pageimages API 2. Wikipedia REST summary API 3. Wikimedia Commons search Returns a direct image URL, or None if every source failed. """ if word in MANUAL_IMAGE_URLS: return MANUAL_IMAGE_URLS[word] title = WIKI_TITLES.get(word, word) url = _try_wikipedia_pageimages(title) if url: return url url = _try_wikipedia_rest_summary(title) if url: return url url = _try_commons_search(word) if url: return url return None def download_image(word): """ Fetch real photo using the multi-source fallback chain, save to disk as JPEG. Returns True on success. """ cp = cache_path(word) print(f"[DL] Looking up real photo for: {word}") img_url = get_image_url(word) if not img_url: print(f"[DL] All sources failed for: {word}") log_missing(word, "no image found from any source") return False print(f"[DL] Downloading: {img_url[:80]}...") # Retry the actual byte download up to 2 times (transient network hiccups) for attempt in range(2): try: r = requests.get(img_url, timeout=15, headers=HEADERS) if r.status_code == 200 and r.content: with open(cp, "wb") as f: f.write(r.content) print(f"[DL] Saved {word} ({len(r.content):,} bytes) -> {cp}") return True else: print(f"[DL] Image fetch attempt {attempt+1} failed for {word}: HTTP {r.status_code}") except Exception as e: print(f"[DL] Attempt {attempt+1} error for {word}: {e}") log_missing(word, "image URL found but download failed") return False def load_surface(word): cp = find_cached_image(word) if cp: try: surf = pygame.image.load(cp) return surf.convert() except Exception as e: print(f"[IMG] Corrupt cache for {word}, re-downloading: {e}") try: os.remove(cp) except Exception: pass # Download then load from disk if download_image(word): cp2 = find_cached_image(word) if cp2: try: surf = pygame.image.load(cp2) return surf.convert() except Exception as e: print(f"[IMG] Load error after download for {word}: {e}") return None # ========================================================= # BACKGROUND IMAGE LOADER THREAD # ========================================================= img_req_q = queue.Queue() img_res_q = queue.Queue() def image_loader_worker(): while True: word = img_req_q.get() if word is None: break surface = load_surface(word) img_res_q.put((word, surface)) img_thread = threading.Thread(target=image_loader_worker, daemon=True) img_thread.start() # ========================================================= # TEXT TO SPEECH # ========================================================= tts_engine = pyttsx3.init() tts_engine.setProperty('rate', 155) tts_engine.setProperty('volume', 1.0) speech_q = queue.Queue() def speech_worker(): while True: text = speech_q.get() if text is None: break tts_engine.say(text) tts_engine.runAndWait() speech_thread = threading.Thread(target=speech_worker, daemon=True) speech_thread.start() def speak(text): while not speech_q.empty(): try: speech_q.get_nowait() except Exception: break speech_q.put(text) # ========================================================= # PYGAME INIT # ========================================================= pygame.init() screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) pygame.display.set_caption("Kid's Learning App") clock = pygame.time.Clock() W, H = screen.get_size() # Make sure the mouse cursor is visible and using the normal system # arrow (fullscreen apps sometimes hide it or leave a stale cursor image). pygame.mouse.set_visible(True) pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_ARROW) # Fonts f_letter = pygame.font.Font(None, 280) f_word = pygame.font.Font(None, 82) f_main = pygame.font.Font(None, 180) f_small = pygame.font.Font(None, 40) f_hint = pygame.font.Font(None, 36) # Colors WHITE = (255, 255, 255) SOFT_BG = (240, 248, 255) GRAY = (225, 225, 225) DARK = (60, 60, 80 ) BLUE = (50, 100, 255) PANEL_L = (214, 234, 255) PANEL_R = (248, 248, 248) BORDER = (190, 205, 230) # ========================================================= # MODES & TYPING STATE # ========================================================= MODE_TYPING = "typing" MODE_ALPHABET = "alphabet" current_mode = MODE_TYPING lines = [[]] cursor_line = 0 # which line the text cursor is on cursor_col = 0 # which character position within that line caps_lock = False click_targets = [] # rebuilt every frame: (pygame.Rect, line_idx, col_idx) PAD_X = 50 PAD_Y = 100 LINE_SPC= 30 lh = f_main.get_height() + LINE_SPC max_vis = (H - 180) // lh # ========================================================= # ALPHABET STATE # ========================================================= cur_letter = None cur_word = None cur_surface = None is_loading = False mem_cache = {} # Per-letter shuffle-bag so words don't repeat until all # words for that letter have been shown once. letter_word_queues = {} _lcol = {} def letter_color(letter): if letter not in _lcol: random.seed(ord(letter) * 31) _lcol[letter] = (random.randint(20,170), random.randint(20,170), random.randint(20,170)) random.seed() return _lcol[letter] def random_color(): return (random.randint(40,220), random.randint(40,220), random.randint(40,220)) def get_next_word(letter): """ Returns the next word for a letter without repeating any word until every word for that letter has been shown once. Also avoids immediately repeating the last-shown word when the bag reshuffles. """ words = ALPHABET_WORDS[letter] if len(words) <= 1: return words[0] bag = letter_word_queues.get(letter, []) if not bag: bag = words[:] random.shuffle(bag) # avoid picking the same word that was just shown for this letter if cur_letter == letter and cur_word is not None and bag[0] == cur_word: bag.append(bag.pop(0)) word = bag.pop(0) letter_word_queues[letter] = bag return word def show_alphabet(letter): global cur_letter, cur_word, cur_surface, is_loading letter = letter.upper() if letter not in ALPHABET_WORDS: return cur_letter = letter cur_word = get_next_word(letter) cur_surface = None if letter in mem_cache and cur_word in mem_cache[letter]: cur_surface = mem_cache[letter][cur_word] is_loading = False else: is_loading = True img_req_q.put(cur_word) speak(f"{letter} … {letter} for {cur_word}") def draw_alphabet_screen(): global cur_surface, is_loading # Collect finished downloads while not img_res_q.empty(): word, surf = img_res_q.get_nowait() if word == cur_word: cur_surface = surf is_loading = False mem_cache.setdefault(cur_letter, {})[word] = surf half = W // 2 top = 80 bot = H - 70 mid_y = (top + bot) // 2 # ── Left panel: big letter + label ── pygame.draw.rect(screen, PANEL_L, (0, top, half, bot - top)) ls = f_letter.render(cur_letter, True, letter_color(cur_letter)) lw, lh2 = ls.get_size() screen.blit(ls, (half//2 - lw//2, mid_y - lh2//2 - 45)) if cur_word: ws = f_word.render(f"{cur_letter} for {cur_word}", True, (30, 50, 120)) screen.blit(ws, (half//2 - ws.get_width()//2, mid_y + lh2//2 - 20)) # ── Right panel: real photo ── rx, ry = half + 16, top + 16 rw, rh = W - half - 32, bot - top - 32 pygame.draw.rect(screen, PANEL_R, (rx, ry, rw, rh), border_radius=18) pygame.draw.rect(screen, BORDER, (rx, ry, rw, rh), 3, border_radius=18) if is_loading: dots = "." * ((pygame.time.get_ticks() // 350) % 5) ls2 = f_word.render(f"Loading{dots}", True, DARK) screen.blit(ls2, (rx + rw//2 - ls2.get_width()//2, ry + rh//2 - ls2.get_height()//2)) elif cur_surface: iw, ih = cur_surface.get_size() scale = min((rw - 20) / iw, (rh - 20) / ih) nw, nh = int(iw * scale), int(ih * scale) scaled = pygame.transform.smoothscale(cur_surface, (nw, nh)) screen.blit(scaled, (rx + (rw - nw)//2, ry + (rh - nh)//2)) else: ns = f_hint.render("Image not available — check internet", True, DARK) screen.blit(ns, (rx + rw//2 - ns.get_width()//2, ry + rh//2)) hs = f_hint.render( "Press the same letter again for another word | F1 = Typing | F12 = Exit", True, (120, 120, 160)) screen.blit(hs, (W//2 - hs.get_width()//2, H - 55)) # ========================================================= # TYPING HELPERS # ========================================================= def insert_char(ch): global cursor_col lines[cursor_line].insert(cursor_col, (ch, random_color())) cursor_col += 1 def new_line(): global cursor_line, cursor_col line = lines[cursor_line] remainder = line[cursor_col:] del line[cursor_col:] lines.insert(cursor_line + 1, remainder) cursor_line += 1 cursor_col = 0 def backspace(): global cursor_line, cursor_col if cursor_col > 0: del lines[cursor_line][cursor_col - 1] cursor_col -= 1 elif cursor_line > 0: prev_len = len(lines[cursor_line - 1]) lines[cursor_line - 1].extend(lines[cursor_line]) del lines[cursor_line] cursor_line -= 1 cursor_col = prev_len def delete_forward(): line = lines[cursor_line] if cursor_col < len(line): del line[cursor_col] elif cursor_line < len(lines) - 1: lines[cursor_line].extend(lines[cursor_line + 1]) del lines[cursor_line + 1] def move_left(): global cursor_line, cursor_col if cursor_col > 0: cursor_col -= 1 elif cursor_line > 0: cursor_line -= 1 cursor_col = len(lines[cursor_line]) def move_right(): global cursor_line, cursor_col if cursor_col < len(lines[cursor_line]): cursor_col += 1 elif cursor_line < len(lines) - 1: cursor_line += 1 cursor_col = 0 def move_up(): global cursor_line, cursor_col if cursor_line > 0: cursor_line -= 1 cursor_col = min(cursor_col, len(lines[cursor_line])) def move_down(): global cursor_line, cursor_col if cursor_line < len(lines) - 1: cursor_line += 1 cursor_col = min(cursor_col, len(lines[cursor_line])) def move_home(): global cursor_col cursor_col = 0 def move_end(): global cursor_col cursor_col = len(lines[cursor_line]) def hit_test(mx, my): """ Given a mouse-click point, return the (line, col) it's closest to, so clicking anywhere in the text places the cursor right there. """ if not click_targets: return None for rect, li, col in click_targets: if rect.collidepoint(mx, my): return li, col # No exact hit (e.g. clicked slightly off a glyph) — snap to nearest. best, best_dist = None, None for rect, li, col in click_targets: cx, cy = rect.center d = (cx - mx) ** 2 + (cy - my) ** 2 if best_dist is None or d < best_dist: best_dist, best = d, (li, col) return best def draw_typing(): global click_targets click_targets = [] glyph_h = f_main.get_height() total = len(lines) # Keep whichever window of lines includes the cursor's line visible, # instead of always pinning to the bottom. if total <= max_vis: start = 0 else: start = cursor_line - max_vis // 2 start = max(0, min(start, total - max_vis)) end = min(total, start + max_vis) y = PAD_Y cursor_screen_pos = None for li in range(start, end): line = lines[li] x = PAD_X row_y = y if not line: # Empty line still needs a clickable/placeable spot. click_targets.append((pygame.Rect(x, row_y, 14, glyph_h), li, 0)) if li == cursor_line: cursor_screen_pos = (x, row_y) else: for ci, (ch, col) in enumerate(line): s = f_main.render(ch, True, col) if x + s.get_width() > W - 50: x = PAD_X y += lh row_y = y rect = pygame.Rect(x, row_y, s.get_width(), s.get_height()) click_targets.append((rect, li, ci)) if li == cursor_line and ci == cursor_col: cursor_screen_pos = (x, row_y) screen.blit(s, (x, row_y)) x += s.get_width() + 10 # Target for placing the cursor at the very end of the line. click_targets.append((pygame.Rect(x, row_y, 14, glyph_h), li, len(line))) if li == cursor_line and cursor_col == len(line): cursor_screen_pos = (x, row_y) y += lh # Blinking text cursor if cursor_screen_pos and (pygame.time.get_ticks() // 500) % 2 == 0: cx, cy = cursor_screen_pos pygame.draw.line(screen, (30, 30, 30), (cx, cy), (cx, cy + glyph_h), 3) def draw_status(): pygame.draw.rect(screen, GRAY, (0, H-60, W, 60)) caps = "CAPS: ON" if caps_lock else "CAPS: OFF" info = f"{caps} | Click a line to edit it, use arrow keys to move | F1 = Typing | F2 = Alphabet | F12 = Exit" screen.blit(f_small.render(info, True, DARK), (20, H - 43)) # ========================================================= # MAIN LOOP # ========================================================= running = True while running: clock.tick(60) screen.fill(SOFT_BG) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if current_mode == MODE_TYPING: result = hit_test(*event.pos) if result: cursor_line, cursor_col = result if event.type == pygame.KEYDOWN: if event.key == pygame.K_F1: current_mode = MODE_TYPING; speak("Typing Mode"); continue if event.key == pygame.K_F2: current_mode = MODE_ALPHABET; speak("Alphabet Mode"); continue if event.key == pygame.K_F12: running = False; continue if event.key == pygame.K_CAPSLOCK: caps_lock = not caps_lock; continue # ── Typing mode ── if current_mode == MODE_TYPING: if event.key == pygame.K_RETURN: new_line(); speak("Next Line"); continue if event.key == pygame.K_SPACE: insert_char(" "); continue if event.key == pygame.K_BACKSPACE: backspace(); continue if event.key == pygame.K_DELETE: delete_forward(); continue if event.key == pygame.K_LEFT: move_left(); continue if event.key == pygame.K_RIGHT: move_right(); continue if event.key == pygame.K_UP: move_up(); continue if event.key == pygame.K_DOWN: move_down(); continue if event.key == pygame.K_HOME: move_home(); continue if event.key == pygame.K_END: move_end(); continue ch = event.unicode if ch and (ch.strip() or ch == " "): if ch.isalpha(): ch = ch.upper() if (caps_lock or pygame.key.get_mods() & pygame.KMOD_SHIFT) else ch.lower() insert_char(ch) speak(ch) # ── Alphabet mode ── elif current_mode == MODE_ALPHABET: ch = event.unicode if ch and ch.isalpha(): show_alphabet(ch) # ── Mode label ── screen.blit(f_small.render(f"MODE : {current_mode.upper()}", True, BLUE), (20, 20)) # ── Draw content ── if current_mode == MODE_TYPING: draw_typing() elif current_mode == MODE_ALPHABET: if cur_letter: draw_alphabet_screen() else: ps = f_word.render("Press any letter key!", True, (80, 80, 180)) screen.blit(ps, (W//2 - ps.get_width()//2, H//2 - 40)) draw_status() pygame.display.flip() # ── Cleanup ── speech_q.put(None) img_req_q.put(None) pygame.quit()