Spaces:
Build error
Build error
| 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 | |
| ###################################### | |
| 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 | |
| ###################################### | |
| 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/canvas-confetti@1.5.1/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. | |
| 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 | |
| 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 | |
| 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() | |