FlashX / app.py
Pavaas's picture
Create app.py
1f7d5fc verified
import streamlit as st
import genanki
import uuid
import base64
import fitz # PyMuPDF
from youtube_transcript_api import YouTubeTranscriptApi
from PIL import Image
import io
# === CONFIG ===
st.set_page_config(page_title="FlashCardX", layout="wide")
# === THEME TOGGLE ===
if 'dark_mode' not in st.session_state:
st.session_state.dark_mode = False
def toggle_theme():
st.session_state.dark_mode = not st.session_state.dark_mode
theme_btn = "๐ŸŒ™ Dark Mode" if not st.session_state.dark_mode else "โ˜€๏ธ Light Mode"
st.sidebar.button(theme_btn, on_click=toggle_theme)
bg_color = "#1e1e1e" if st.session_state.dark_mode else "#ffffff"
txt_color = "#f1f1f1" if st.session_state.dark_mode else "#000000"
st.markdown(f"""<style>.stApp {{ background-color: {bg_color}; color: {txt_color}; }}</style>""", unsafe_allow_html=True)
# === STATE ===
if 'deck' not in st.session_state:
st.session_state.deck = []
# === HEADER ===
st.title("๐Ÿง  FlashCardX โ€” Flashka Clone (Offline, Free)")
# === MANUAL Q/A FLASHCARD ENTRY ===
st.header("โž• Add Flashcard (Manual)")
q = st.text_area("Question")
a = st.text_area("Answer")
if st.button("Add Flashcard"):
if q.strip() and a.strip():
st.session_state.deck.append({'type': 'basic', 'q': q.strip(), 'a': a.strip()})
st.success("Card added.")
else:
st.warning("Both fields are required.")
# === MCQ FLASHCARDS ===
st.header("๐Ÿ“ Add MCQ Card")
mcq_q = st.text_input("MCQ Question")
opt1 = st.text_input("Option A")
opt2 = st.text_input("Option B")
opt3 = st.text_input("Option C")
opt4 = st.text_input("Option D")
mcq_ans = st.selectbox("Correct Answer", ["A", "B", "C", "D"])
if st.button("Add MCQ"):
if all([mcq_q, opt1, opt2, opt3, opt4]):
st.session_state.deck.append({
'type': 'mcq',
'q': mcq_q,
'options': [opt1, opt2, opt3, opt4],
'a': mcq_ans
})
st.success("MCQ added.")
else:
st.warning("All options must be filled.")
# === PDF UPLOAD TO FLASHCARDS ===
st.header("๐Ÿ“„ Upload PDF to Generate Flashcards")
pdf_file = st.file_uploader("Upload PDF", type=['pdf'])
if pdf_file:
doc = fitz.open(stream=pdf_file.read(), filetype="pdf")
for page in doc:
text = page.get_text()
lines = [l.strip() for l in text.split('\n') if l.strip()]
for i in range(0, len(lines) - 1, 2):
st.session_state.deck.append({'type': 'basic', 'q': lines[i], 'a': lines[i + 1]})
st.success("PDF parsed and flashcards added.")
# === YOUTUBE TO FLASHCARDS ===
st.header("๐ŸŽฅ YouTube โ†’ Flashcards")
yt_url = st.text_input("YouTube video link")
if st.button("Extract Transcript Cards"):
try:
video_id = yt_url.split("v=")[-1].split("&")[0]
transcript = YouTubeTranscriptApi.get_transcript(video_id)
for item in transcript[:10]: # limit to 10 cards
text = item['text']
st.session_state.deck.append({'type': 'basic', 'q': f"What is said here?", 'a': text})
st.success("Transcript converted to flashcards.")
except Exception as e:
st.error(f"Error: {e}")
# === IMAGE OCCLUSION ===
st.header("๐Ÿ–ผ๏ธ Image Occlusion Flashcard")
img_file = st.file_uploader("Upload Image", type=['png', 'jpg', 'jpeg'])
if img_file:
image = Image.open(img_file)
st.image(image, caption="Original Image")
label = st.text_input("Describe hidden info")
if st.button("Add Image Occlusion Card"):
img_bytes = io.BytesIO()
image.save(img_bytes, format='PNG')
b64_img = base64.b64encode(img_bytes.getvalue()).decode()
q = f'<img src="data:image/png;base64,{b64_img}"><br><b>What is hidden?</b>'
a = label
st.session_state.deck.append({'type': 'basic', 'q': q, 'a': a})
st.success("Image occlusion card added.")
# === SEARCH ===
st.header("๐Ÿ” Search Deck")
search_term = st.text_input("Enter keyword to filter")
filtered_deck = [c for c in st.session_state.deck if search_term.lower() in c['q'].lower()]
# === DECK VIEW ===
st.header("๐Ÿ“š Flashcard Deck Viewer")
if filtered_deck:
for i, card in enumerate(filtered_deck):
with st.expander(f"Card {i+1}"):
if card['type'] == 'basic':
st.markdown(f"**Q:** {card['q']}", unsafe_allow_html=True)
st.markdown(f"**A:** {card['a']}", unsafe_allow_html=True)
elif card['type'] == 'mcq':
st.markdown(f"**Q:** {card['q']}")
for idx, opt in zip(['A', 'B', 'C', 'D'], card['options']):
st.markdown(f"- {idx}. {opt}")
st.markdown(f"**Answer:** {card['a']}")
if st.button(f"๐Ÿ—‘๏ธ Delete", key=f"del_{i}"):
st.session_state.deck.remove(card)
st.experimental_rerun()
else:
st.info("No matching flashcards.")
# === GAMIFIED REVIEW ===
st.header("๐ŸŽฎ Gamified Review")
if st.button("Start Review"):
for card in st.session_state.deck:
if card['type'] == 'basic':
st.info(f"Q: {card['q']}")
st.text_input("Your Answer")
st.markdown(f"**Correct:** {card['a']}")
elif card['type'] == 'mcq':
st.info(card['q'])
choice = st.radio("Choose", ["A", "B", "C", "D"], key=uuid.uuid4())
st.markdown(f"**Answer:** {card['a']}")
# === EXPORT TO ANKI ===
st.sidebar.header("๐Ÿ“ฅ Export to Anki")
deck_name = st.sidebar.text_input("Deck Name", value="MyFlashDeck")
if st.sidebar.button("Download .apkg"):
model_id = int(uuid.uuid4()) >> 64
model = genanki.Model(
model_id,
'FlashCardX Model',
fields=[{'name': 'Question'}, {'name': 'Answer'}],
templates=[{
'name': 'Card Template',
'qfmt': '{{Question}}',
'afmt': '{{FrontSide}}<hr>{{Answer}}',
}],
)
my_deck = genanki.Deck(int(uuid.uuid4()) >> 64, deck_name)
for card in st.session_state.deck:
if card['type'] == 'basic':
my_deck.add_note(genanki.Note(model=model, fields=[card['q'], card['a']]))
elif card['type'] == 'mcq':
opts = "\n".join([f"{i}. {opt}" for i, opt in zip("ABCD", card['options'])])
q = f"{card['q']}\n\n{opts}"
a = f"Correct Answer: {card['a']}"
my_deck.add_note(genanki.Note(model=model, fields=[q, a]))
pkg = genanki.Package(my_deck)
fname = f"{deck_name.replace(' ', '_')}.apkg"
pkg.write_to_file(fname)
with open(fname, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
href = f'<a href="data:application/octet-stream;base64,{b64}" download="{fname}">๐Ÿ“ฆ Download {fname}</a>'
st.sidebar.markdown(href, unsafe_allow_html=True)
# === FOOTER ===
st.markdown("---")
st.caption("๐Ÿง  FlashCardX ยท A clean Flashka-style clone ยท Powered by Streamlit")