KASOTI / app.py
iisadia's picture
Update app.py
0bdde1a verified
import streamlit as st
import time
import requests
from streamlit.components.v1 import html
import os
from dotenv import load_dotenv
# New imports for voice input
import torchaudio
import numpy as np
import torch
from io import BytesIO
import hashlib
from audio_recorder_streamlit import audio_recorder
from transformers import pipeline
######################################
# Voice Input Helper Functions
######################################
@st.cache_resource
def load_voice_model():
# Loading the Whisper model (which automatically detects both English and Urdu)
return pipeline("automatic-speech-recognition", model="openai/whisper-base")
def process_audio(audio_bytes):
waveform, sample_rate = torchaudio.load(BytesIO(audio_bytes))
if waveform.shape[0] > 1: # Convert stereo to mono
waveform = torch.mean(waveform, dim=0, keepdim=True)
if sample_rate != 16000: # Resample to 16kHz if needed
resampler = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=16000)
waveform = resampler(waveform)
return {"raw": waveform.numpy().squeeze(), "sampling_rate": 16000}
def get_voice_transcription(state_key):
"""Display audio recorder for a given key.
If new audio is recorded, transcribe it and update the session state.
"""
if state_key not in st.session_state:
st.session_state[state_key] = ""
# Use a unique key for the recorder widget
audio_bytes = audio_recorder(key=state_key + "_audio",
pause_threshold=0.8,
text="Speak to type",
recording_color="#e8b62c",
neutral_color="#6aa36f")
if audio_bytes:
current_hash = hashlib.md5(audio_bytes).hexdigest()
last_hash_key = state_key + "_last_hash"
if st.session_state.get(last_hash_key, "") != current_hash:
st.session_state[last_hash_key] = current_hash
try:
audio_input = process_audio(audio_bytes)
whisper = load_voice_model()
transcribed_text = whisper(audio_input)["text"]
st.info(f"๐Ÿ“ Transcribed: {transcribed_text}")
# Append (or set) new transcription
st.session_state[state_key] += (" " + transcribed_text).strip()
st.experimental_rerun()
except Exception as e:
st.error(f"Voice input error: {str(e)}")
return st.session_state[state_key]
######################################
# Existing Game Helper Functions
######################################
@st.cache_resource
def get_help_agent():
from transformers import pipeline
# Using BlenderBot 400M Distill as the public conversational model (used elsewhere)
return pipeline("conversational", model="facebook/blenderbot-400M-distill")
def inject_custom_css():
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css');
* { font-family: 'Inter', sans-serif; }
body { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); }
.title { font-size: 2.8rem !important; font-weight: 800 !important;
background: linear-gradient(45deg, #6C63FF, #3B82F6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
text-align: center; margin: 1rem 0; letter-spacing: -1px; }
.subtitle { font-size: 1.1rem !important; text-align: center;
color: #64748B !important; margin-bottom: 2.5rem; animation: fadeInSlide 1s ease; }
.question-box { background: white; border-radius: 20px; padding: 2rem; margin: 1.5rem 0;
box-shadow: 0 10px 25px rgba(0,0,0,0.08); border: 1px solid #e2e8f0;
position: relative; transition: transform 0.2s ease; color: black; }
.question-box:hover { transform: translateY(-3px); }
.question-box::before { content: "๐Ÿ•น๏ธ"; position: absolute; left: -15px; top: -15px;
background: white; border-radius: 50%; padding: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1); font-size: 1.2rem; }
.input-box { background: white; border-radius: 12px; padding: 1.5rem; margin: 1rem 0;
box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.stTextInput input { border: 2px solid #e2e8f0 !important; border-radius: 10px !important;
padding: 12px 16px !important; transition: all 0.3s ease !important; }
.stTextInput input:focus { border-color: #6C63FF !important;
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.2) !important; }
button { background: linear-gradient(45deg, #6C63FF, #3B82F6) !important;
color: white !important; border: none !important; border-radius: 10px !important;
padding: 12px 24px !important; font-weight: 600 !important;
transition: all 0.3s ease !important; }
button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(108, 99, 255, 0.3) !important; }
.final-reveal { animation: fadeInUp 1s ease; font-size: 2.8rem;
background: linear-gradient(45deg, #6C63FF, #3B82F6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
text-align: center; margin: 2rem 0; font-weight: 800; }
.help-chat { background: rgba(255,255,255,0.9); backdrop-filter: blur(10px);
border-radius: 15px; padding: 1rem; margin: 1rem 0;
box-shadow: 0 8px 30px rgba(0,0,0,0.12); }
@keyframes fadeInSlide { 0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); } }
@keyframes fadeInUp { 0% { opacity: 0; transform: translateY(30px); }
100% { opacity: 1; transform: translateY(0); } }
.progress-bar { height: 6px; background: #e2e8f0; border-radius: 3px;
margin: 1.5rem 0; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #6C63FF, #3B82F6);
transition: width 0.5s ease; }
.question-count { color: #6C63FF; font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
/* Add these new styles for the info modal */
.info-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 2rem;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
z-index: 1000;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.info-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
}
.tech-item {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.tech-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.tech-title {
color: #6C63FF;
font-weight: 600;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.close-modal-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #64748B;
}
</style>
""", unsafe_allow_html=True)
def show_confetti():
html("""
<canvas id="confetti-canvas" class="confetti"></canvas>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script>
<script>
const count = 200;
const defaults = {
origin: { y: 0.7 },
zIndex: 1050
};
function fire(particleRatio, opts) {
confetti(Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio)
}));
}
fire(0.25, { spread: 26, startVelocity: 55 });
fire(0.2, { spread: 60 });
fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 });
fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 });
fire(0.1, { spread: 120, startVelocity: 45 });
</script>
""")
def ask_llama(conversation_history, category, is_final_guess=False):
api_url = "https://api.groq.com/openai/v1/chat/completions"
headers = {
"Authorization": "Bearer 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",
"Content-Type": "application/json"
}
system_prompt = f"""You're playing 20 questions to guess a {category}. Follow these rules:
1. Ask strategic, non-repeating yes/no questions that narrow down possibilities
2. Consider all previous answers carefully before asking next question
3. If you're very confident (80%+ sure), respond with "Final Guess: [your guess]"
4. For places: ask about continent, climate, famous landmarks, country, city or population
5. For people: ask about fictional or real, profession, gender, alive/dead, nationality, or fame
6. For objects: ask about size, color, usage, material, or where it's found
7. Never repeat questions and always make progress toward guessing"""
if is_final_guess:
prompt = f"""Based on these answers about a {category}, provide ONLY your final guess with no extra text:
{conversation_history}"""
else:
prompt = "Ask your next strategic yes/no question that will best narrow down the possibilities."
messages = [
{"role": "system", "content": system_prompt},
*conversation_history,
{"role": "user", "content": prompt}
]
data = {
"model": "llama-3.3-70b-versatile",
"messages": messages,
"temperature": 0.7 if is_final_guess else 0.8,
"max_tokens": 100
}
try:
response = requests.post(api_url, headers=headers, json=data)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
except Exception as e:
st.error(f"Error calling Llama API: {str(e)}")
return "Could not generate question"
MISTRAL_API_KEY = "wm5eLl09b9I9cOxR3E9n5rrRr1CRQQjn"
def ask_help_agent(query):
try:
url = "https://api.mistral.ai/v1/chat/completions"
headers = {
"Authorization": f"Bearer {MISTRAL_API_KEY}",
"Content-Type": "application/json"
}
system_message = "You are a friendly Chatbot."
messages = [{"role": "system", "content": system_message}]
if "help_conversation" in st.session_state:
for msg in st.session_state.help_conversation:
if msg.get("query"):
messages.append({"role": "user", "content": msg["query"]})
if msg.get("response"):
messages.append({"role": "assistant", "content": msg["response"]})
messages.append({"role": "user", "content": query})
payload = {
"model": "mistral-tiny",
"messages": messages,
"temperature": 0.7,
"top_p": 0.95
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
return result["choices"][0]["message"]["content"]
else:
return f"API Error {response.status_code}: {response.text}"
except Exception as e:
return f"Error in help agent: {str(e)}"
def show_techniques_modal():
# Use Streamlit's expander with markdown
with st.expander("โ„น๏ธ Project Techniques & Limitations", expanded=True):
st.markdown("""
**AI Models Used:**
1. Groq Llama 3.3-70B - For generating strategic questions and final guesses
2. Mistral Tiny - Powers the help chat assistant
3. OpenAI Whisper - Converts speech to text in real-time
4. Hard Prompt Tuning - Carefully engineered prompts to optimize model performance
**Known Limitations:**
1. Voice input may take 5-10 seconds to process sentences, which is fine. (Whisper model trained on long sentences, so it's very accurate, but good for a sentence, but not for a word)
2. Single words (like "yes", "object") may take 10-20 minutes, which is irritating.
3. Language Support - While Whisper understands and writes Urdu, but the game only supports English responses
""")
if st.button("Close", key="modal_close_btn"):
pass # The expander will automatically close
######################################
# Main Game Logic with Voice Integration
######################################
def main():
inject_custom_css()
st.markdown('<div class="title">KASOTI</div>', unsafe_allow_html=True)
st.markdown('<div class="subtitle">AI-Powered Guessing Game Challenge</div>', unsafe_allow_html=True)
if st.button("โ„น๏ธ Project Techniques & Limitations", key="info_btn"):
show_techniques_modal()
if 'game_state' not in st.session_state:
st.session_state.game_state = "start"
st.session_state.questions = []
st.session_state.current_q = 0
st.session_state.answers = []
st.session_state.conversation_history = []
st.session_state.category = None
st.session_state.final_guess = None
st.session_state.help_conversation = [] # separate history for help agent
# Start screen with enhanced layout
if st.session_state.game_state == "start":
with st.container():
st.markdown("""
<div class="question-box">
<h3 style="color: #6C63FF; margin-bottom: 1.5rem;">๐ŸŽฎ Welcome to KASOTI</h3>
<p style="line-height: 1.6; color: #64748B;">
Think of something and I'll try to guess it in 20 questions or less!<br>
Choose from these categories:
</p>
<div style="display: grid; gap: 1rem; margin: 2rem 0;">
<div style="padding: 1.5rem; background: #f8f9fa; border-radius: 12px;">
<h4 style="margin: 0; color: #6C63FF;">๐Ÿง‘ Person</h4>
<p style="margin: 0.5rem 0 0; color: #64748B;">Celebrity, fictional character, historical figure</p>
</div>
<div style="padding: 1.5rem; background: #f8f9fa; border-radius: 12px;">
<h4 style="margin: 0; color: #6C63FF;">๐ŸŒ Place</h4>
<p style="margin: 0.5rem 0 0; color: #64748B;">City, country, landmark, geographical location</p>
</div>
<div style="padding: 1.5rem; background: #f8f9fa; border-radius: 12px;">
<h4 style="margin: 0; color: #6C63FF;">๐ŸŽฏ Object</h4>
<p style="margin: 0.5rem 0 0; color: #64748B;">Everyday item, tool, vehicle</p>
</div>
</div>
</div>
""", unsafe_allow_html=True)
with st.form("start_form"):
# --- Voice Input for Category ---
st.markdown("#### Use Voice (English/Urdu) for Category Input")
voice_category = get_voice_transcription("voice_category")
# The text input now defaults to any spoken words
category_input = st.text_input("Enter category (person/place/object):",
value=voice_category.strip(),
key="category_input").strip().lower()
if st.form_submit_button("Start Game"):
if not category_input:
st.error("Please enter a category!")
elif category_input not in ["person", "place", "object"]:
st.error("Please enter either 'person', 'place', or 'object'!")
else:
st.session_state.category = category_input
first_question = ask_llama([
{"role": "user", "content": "Ask your first strategic yes/no question."}
], category_input)
st.session_state.questions = [first_question]
st.session_state.conversation_history = [
{"role": "assistant", "content": first_question}
]
st.session_state.game_state = "gameplay"
st.experimental_rerun()
# Gameplay screen with progress bar
elif st.session_state.game_state == "gameplay":
with st.container():
progress = (st.session_state.current_q + 1) / 20
st.markdown(f"""
<div class="question-count">QUESTION {st.session_state.current_q + 1} OF 20</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {progress * 100}%"></div>
</div>
""", unsafe_allow_html=True)
current_question = st.session_state.questions[st.session_state.current_q]
st.markdown(f'''
<div class="question-box">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
<div style="background: #6C63FF; width: 40px; height: 40px; border-radius: 50%;
display: flex; align-items: center; justify-content: center; color: white;">
<i class="fas fa-robot"></i>
</div>
<h3 style="margin: 0; color: #1E293B;">AI Question</h3>
</div>
<p style="font-size: 1.1rem; line-height: 1.6; color: #1E293B;">{current_question}</p>
</div>
''', unsafe_allow_html=True)
if "Final Guess:" in current_question:
st.session_state.final_guess = current_question.split("Final Guess:")[1].strip()
st.session_state.game_state = "confirm_guess"
st.experimental_rerun()
with st.form("answer_form"):
# --- Voice Input for Answer ---
st.markdown("#### Use Voice (English/Urdu) for Your Answer")
voice_answer = get_voice_transcription("voice_answer")
answer_input = st.text_input("Your answer (yes/no/both):",
value=voice_answer.strip(),
key=f"answer_{st.session_state.current_q}").strip().lower()
if st.form_submit_button("Submit"):
if answer_input not in ["yes", "no", "both"]:
st.error("Please answer with 'yes', 'no', or 'both'!")
else:
st.session_state.answers.append(answer_input)
st.session_state.conversation_history.append(
{"role": "user", "content": answer_input}
)
next_response = ask_llama(
st.session_state.conversation_history,
st.session_state.category
)
if "Final Guess:" in next_response:
st.session_state.final_guess = next_response.split("Final Guess:")[1].strip()
st.session_state.game_state = "confirm_guess"
else:
st.session_state.questions.append(next_response)
st.session_state.conversation_history.append(
{"role": "assistant", "content": next_response}
)
st.session_state.current_q += 1
if st.session_state.current_q >= 20:
st.session_state.game_state = "result"
st.experimental_rerun()
with st.expander("Need Help? Chat with AI Assistant"):
# --- Voice Input for Help Query ---
st.markdown("#### Use Voice (English/Urdu) for Help Query")
voice_help = get_voice_transcription("voice_help")
help_query = st.text_input("Enter your help query:",
value=voice_help.strip(),
key="help_query")
if st.button("Send", key="send_help"):
if help_query:
help_response = ask_help_agent(help_query)
st.session_state.help_conversation.append({"query": help_query, "response": help_response})
else:
st.error("Please enter a query!")
if st.session_state.help_conversation:
for msg in st.session_state.help_conversation:
st.markdown(f"**You:** {msg['query']}")
st.markdown(f"**Help Assistant:** {msg['response']}")
elif st.session_state.game_state == "confirm_guess":
st.markdown(f'''
<div class="question-box">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
<div style="background: #6C63FF; width: 40px; height: 40px; border-radius: 50%;
display: flex; align-items: center; justify-content: center; color: white;">
<i class="fas fa-lightbulb"></i>
</div>
<h3 style="margin: 0; color: #1E293B;">AI's Final Guess</h3>
</div>
<p style="font-size: 1.2rem; line-height: 1.6; color: #1E293B;">
Is it <strong style="color: #6C63FF;">{st.session_state.final_guess}</strong>?
</p>
</div>
''', unsafe_allow_html=True)
with st.form("confirm_form"):
confirm_input = st.text_input("Type your answer (yes/no/both):", key="confirm_input").strip().lower()
if st.form_submit_button("Submit"):
if confirm_input not in ["yes", "no", "both"]:
st.error("Please answer with 'yes', 'no', or 'both'!")
else:
if confirm_input == "yes":
st.session_state.game_state = "result"
st.experimental_rerun()
st.stop()
else:
st.session_state.conversation_history.append(
{"role": "user", "content": "no"}
)
st.session_state.game_state = "gameplay"
next_response = ask_llama(
st.session_state.conversation_history,
st.session_state.category
)
st.session_state.questions.append(next_response)
st.session_state.conversation_history.append(
{"role": "assistant", "content": next_response}
)
st.session_state.current_q += 1
st.experimental_rerun()
elif st.session_state.game_state == "result":
if not st.session_state.final_guess:
qa_history = "\n".join(
[f"Q{i+1}: {q}\nA: {a}"
for i, (q, a) in enumerate(zip(st.session_state.questions, st.session_state.answers))]
)
final_guess = ask_llama(
[{"role": "user", "content": qa_history}],
st.session_state.category,
is_final_guess=True
)
st.session_state.final_guess = final_guess.split("Final Guess:")[-1].strip()
show_confetti()
st.markdown(f'<div class="final-reveal">๐ŸŽ‰ It\'s...</div>', unsafe_allow_html=True)
time.sleep(1)
st.markdown(f'<div class="final-reveal" style="font-size:3.5rem;color:#6C63FF;">{st.session_state.final_guess}</div>',
unsafe_allow_html=True)
st.markdown(f"<p style='text-align:center; color:#64748B;'>Guessed in {len(st.session_state.questions)} questions</p>",
unsafe_allow_html=True)
if st.button("Play Again", key="play_again"):
st.session_state.clear()
st.experimental_rerun()
if __name__ == "__main__":
main()