|
import gradio as gr |
|
import random |
|
import time |
|
import datetime |
|
import json |
|
import re |
|
from typing import Dict, List, Tuple, Optional |
|
|
|
|
|
transformers_js_code = """ |
|
<script src="https://cdn.jsdelivr.net/npm/@xenova/[email protected]"></script> |
|
<script> |
|
// Initialize Transformers.js pipelines |
|
let sttPipeline = null; |
|
let ttsPipeline = null; |
|
let speakerEmbeddings = null; |
|
|
|
// Load models asynchronously |
|
async function loadModels() { |
|
try { |
|
// Show loading status |
|
document.getElementById("status-text").innerText = "Loading speech models..."; |
|
|
|
// Load speech-to-text model |
|
const { pipeline } = await import('https://cdn.jsdelivr.net/npm/@xenova/[email protected]'); |
|
sttPipeline = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny'); |
|
|
|
// Load text-to-speech model and embeddings |
|
ttsPipeline = await pipeline('text-to-speech', 'Xenova/speecht5_tts'); |
|
const { loadSpeakerEmbeddings } = await import('https://cdn.jsdelivr.net/npm/@xenova/[email protected]'); |
|
speakerEmbeddings = await loadSpeakerEmbeddings('Xenova/speecht5_vc_pt_sd_epoch_1000'); |
|
|
|
console.log('All models loaded successfully'); |
|
document.getElementById("status-text").innerText = "Ready to listen"; |
|
} catch (error) { |
|
console.error('Error loading models:', error); |
|
document.getElementById("status-text").innerText = "Error loading models"; |
|
} |
|
} |
|
|
|
// Transcribe audio using Transformers.js |
|
async function transcribeAudio(audioBlob) { |
|
if (!sttPipeline) { |
|
console.error('Speech-to-text model not loaded'); |
|
return "Speech recognition not ready"; |
|
} |
|
|
|
try { |
|
document.getElementById("status-text").innerText = "Processing speech..."; |
|
const output = await sttPipeline(audioBlob, { |
|
language: 'english', |
|
task: 'transcribe', |
|
}); |
|
document.getElementById("status-text").innerText = "Ready to listen"; |
|
return output.text; |
|
} catch (error) { |
|
console.error('Transcription error:', error); |
|
document.getElementById("status-text").innerText = "Ready to listen"; |
|
return "I couldn't understand that"; |
|
} |
|
} |
|
|
|
// Generate speech using Transformers.js |
|
async function generateSpeech(text) { |
|
if (!ttsPipeline || !speakerEmbeddings) { |
|
console.error('Text-to-speech model not loaded'); |
|
return null; |
|
} |
|
|
|
try { |
|
const audio = await ttsPipeline(text, { |
|
speaker_embeddings: speakerEmbeddings, |
|
}); |
|
return URL.createObjectURL(audio); |
|
} catch (error) { |
|
console.error('Speech generation error:', error); |
|
return null; |
|
} |
|
} |
|
|
|
// Initialize models when page loads |
|
window.addEventListener('DOMContentLoaded', () => { |
|
loadModels(); |
|
console.log('Transformers.js initialized'); |
|
}); |
|
|
|
// Function to handle audio recording and transcription |
|
async function handleAudioRecording(audioBlob) { |
|
const transcript = await transcribeAudio(audioBlob); |
|
if (transcript && transcript !== "I couldn't understand that") { |
|
// Send the transcript to Gradio |
|
const hiddenTextbox = document.querySelector('#hidden-transcript textarea'); |
|
if (hiddenTextbox) { |
|
hiddenTextbox.value = transcript; |
|
hiddenTextbox.dispatchEvent(new Event('input', { bubbles: true })); |
|
hiddenTextbox.dispatchEvent(new Event('change', { bubbles: true })); |
|
} |
|
} |
|
} |
|
|
|
// Function to speak text |
|
async function speakText(text) { |
|
const audioUrl = await generateSpeech(text); |
|
if (audioUrl) { |
|
const audio = new Audio(audioUrl); |
|
audio.play(); |
|
} |
|
} |
|
|
|
// Add event listener for when the bot response updates |
|
function setupResponseObserver() { |
|
const targetNode = document.querySelector('#ai-response textarea'); |
|
if (targetNode) { |
|
const config = { characterData: true, childList: true, subtree: true }; |
|
const callback = function(mutationsList, observer) { |
|
for (const mutation of mutationsList) { |
|
if (mutation.type === 'childList' || mutation.type === 'characterData') { |
|
const responseText = targetNode.value; |
|
if (responseText && !responseText.includes('Hello! I\'m your AppleCare concierge')) { |
|
speakText(responseText); |
|
} |
|
} |
|
} |
|
}; |
|
const observer = new MutationObserver(callback); |
|
observer.observe(targetNode, config); |
|
} |
|
} |
|
|
|
// Initialize when page loads |
|
window.addEventListener('DOMContentLoaded', () => { |
|
loadModels(); |
|
setTimeout(setupResponseObserver, 2000); // Wait for Gradio to render |
|
}); |
|
</script> |
|
""" |
|
|
|
class AppleCareVoiceConcierge: |
|
def __init__(self): |
|
self.conversation_state = { |
|
"step": "greeting", |
|
"device": None, |
|
"issue": None, |
|
"location": None, |
|
"imei": None, |
|
"user_name": None, |
|
"estimated_cost": None, |
|
"nearest_store": None, |
|
"history": [] |
|
} |
|
|
|
|
|
self.stores = { |
|
"10001": {"name": "Apple Fifth Avenue", "address": "767 5th Ave, New York, NY 10153", "phone": "(212) 336-1440"}, |
|
"10029": {"name": "Apple Upper East Side", "address": "940 Madison Ave, New York, NY 10075", "phone": "(212) 284-1800"}, |
|
"90210": {"name": "Apple Beverly Hills", "address": "444 N Rodeo Dr, Beverly Hills, CA 90210", "phone": "(310) 273-3000"}, |
|
"94102": {"name": "Apple Union Square", "address": "300 Post St, San Francisco, CA 94108", "phone": "(415) 392-0202"}, |
|
"60611": {"name": "Apple Michigan Avenue", "address": "401 N Michigan Ave, Chicago, IL 60611", "phone": "(312) 981-4104"}, |
|
"75201": {"name": "Apple Northpark Center", "address": "8687 N Central Expy, Dallas, TX 75225", "phone": "(214) 965-0960"}, |
|
"02116": {"name": "Apple Boylston Street", "address": "815 Boylston St, Boston, MA 02116", "phone": "(617) 385-9400"}, |
|
"98101": {"name": "Apple University Village", "address": "4742 42nd Ave NE, Seattle, WA 98105", "phone": "(206) 892-0076"}, |
|
"33139": {"name": "Apple Lincoln Road", "address": "1021 Lincoln Rd, Miami Beach, FL 33139", "phone": "(305) 421-0200"}, |
|
"30309": {"name": "Apple Lenox Square", "address": "3393 Peachtree Rd NE, Atlanta, GA 30326", "phone": "(404) 816-9500"} |
|
} |
|
|
|
|
|
self.city_mappings = { |
|
"new york": "10001", |
|
"nyc": "10001", |
|
"manhattan": "10001", |
|
"los angeles": "90210", |
|
"la": "90210", |
|
"beverly hills": "90210", |
|
"san francisco": "94102", |
|
"sf": "94102", |
|
"chicago": "60611", |
|
"dallas": "75201", |
|
"boston": "02116", |
|
"seattle": "98101", |
|
"miami": "33139", |
|
"atlanta": "30309" |
|
} |
|
|
|
|
|
self.repair_costs = { |
|
"iphone": { |
|
"screen": "$279", |
|
"battery": "$89", |
|
"camera": "$149", |
|
"water": "$99 diagnostic + repair cost", |
|
"speaker": "$169", |
|
"charging": "$99" |
|
}, |
|
"ipad": { |
|
"screen": "$399", |
|
"battery": "$129", |
|
"camera": "$199", |
|
"water": "$149 diagnostic + repair cost", |
|
"speaker": "$149", |
|
"charging": "$149" |
|
}, |
|
"mac": { |
|
"screen": "$599", |
|
"battery": "$199", |
|
"keyboard": "$249", |
|
"trackpad": "$179", |
|
"water": "$299 diagnostic + repair cost" |
|
}, |
|
"watch": { |
|
"screen": "$249", |
|
"battery": "$79", |
|
"water": "$229 service exchange" |
|
} |
|
} |
|
|
|
def reset_conversation(self): |
|
"""Reset conversation state""" |
|
self.conversation_state = { |
|
"step": "greeting", |
|
"device": None, |
|
"issue": None, |
|
"location": None, |
|
"imei": None, |
|
"user_name": None, |
|
"estimated_cost": None, |
|
"nearest_store": None, |
|
"history": [] |
|
} |
|
|
|
def extract_device_info(self, text: str) -> Optional[str]: |
|
"""Extract device type from user input""" |
|
text_lower = text.lower() |
|
|
|
if any(word in text_lower for word in ["iphone", "phone"]): |
|
return "iphone" |
|
elif any(word in text_lower for word in ["ipad", "tablet"]): |
|
return "ipad" |
|
elif any(word in text_lower for word in ["mac", "macbook", "laptop", "computer", "imac"]): |
|
return "mac" |
|
elif any(word in text_lower for word in ["watch", "apple watch"]): |
|
return "watch" |
|
return None |
|
|
|
def extract_issue_info(self, text: str, device: str) -> Tuple[Optional[str], Optional[str]]: |
|
"""Extract issue type and cost from user input""" |
|
text_lower = text.lower() |
|
|
|
issue_keywords = { |
|
"screen": ["screen", "crack", "broken", "display", "shatter"], |
|
"battery": ["battery", "charge", "power", "drain", "dead"], |
|
"camera": ["camera", "photo", "lens", "focus"], |
|
"water": ["water", "wet", "liquid", "rain", "drop", "spill"], |
|
"speaker": ["speaker", "sound", "audio", "volume"], |
|
"charging": ["charging", "port", "cable", "connector"], |
|
"keyboard": ["keyboard", "key", "typing"], |
|
"trackpad": ["trackpad", "mouse", "cursor", "click"] |
|
} |
|
|
|
for issue_type, keywords in issue_keywords.items(): |
|
if any(keyword in text_lower for keyword in keywords): |
|
if device in self.repair_costs and issue_type in self.repair_costs[device]: |
|
return issue_type, self.repair_costs[device][issue_type] |
|
|
|
return None, None |
|
|
|
def extract_location_info(self, text: str) -> Optional[Dict]: |
|
"""Extract location from user input""" |
|
text_lower = text.lower() |
|
|
|
|
|
zip_match = re.search(r'\b\d{5}\b', text) |
|
if zip_match: |
|
zip_code = zip_match.group() |
|
if zip_code in self.stores: |
|
return self.stores[zip_code] |
|
else: |
|
return self.stores["10001"] |
|
|
|
|
|
for city, zip_code in self.city_mappings.items(): |
|
if city in text_lower: |
|
return self.stores[zip_code] |
|
|
|
return None |
|
|
|
def extract_imei(self, text: str) -> Optional[str]: |
|
"""Extract IMEI or serial number from user input""" |
|
|
|
imei_match = re.search(r'\b[A-Za-z0-9]{10,}\b', text.replace(" ", "")) |
|
if imei_match: |
|
return imei_match.group() |
|
return None |
|
|
|
def generate_appointment_details(self) -> Dict: |
|
"""Generate realistic appointment details""" |
|
|
|
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1) |
|
if tomorrow.weekday() >= 5: |
|
tomorrow += datetime.timedelta(days=(7 - tomorrow.weekday())) |
|
|
|
|
|
time_slots = ["9:00 AM", "10:30 AM", "12:00 PM", "1:30 PM", "3:00 PM", "4:30 PM"] |
|
selected_time = random.choice(time_slots) |
|
|
|
|
|
confirmation = "AC" + "".join(random.choices("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=6)) |
|
|
|
return { |
|
"date": tomorrow.strftime("%A, %B %d, %Y"), |
|
"time": selected_time, |
|
"confirmation": confirmation |
|
} |
|
|
|
def process_conversation(self, user_input: str) -> str: |
|
"""Main conversation processing logic""" |
|
if not user_input.strip(): |
|
return "I didn't catch that. Could you please speak again?" |
|
|
|
|
|
self.conversation_state["history"].append({"role": "user", "content": user_input}) |
|
|
|
current_step = self.conversation_state["step"] |
|
|
|
if current_step == "greeting": |
|
|
|
device = self.extract_device_info(user_input) |
|
if device: |
|
self.conversation_state["device"] = device |
|
self.conversation_state["step"] = "issue_identification" |
|
device_name = device.title() if device != "iphone" else "iPhone" |
|
if device == "mac": |
|
device_name = "Mac" |
|
response = f"I can help with your {device_name}. What seems to be the problem? For example, is the screen damaged, battery issues, or something else?" |
|
else: |
|
response = "I can help with iPhone, iPad, Mac, or Apple Watch repairs. Which device needs assistance today?" |
|
|
|
elif current_step == "issue_identification": |
|
|
|
issue_type, cost = self.extract_issue_info(user_input, self.conversation_state["device"]) |
|
if issue_type and cost: |
|
self.conversation_state["issue"] = issue_type |
|
self.conversation_state["estimated_cost"] = cost |
|
self.conversation_state["step"] = "location_gathering" |
|
device_name = self.conversation_state["device"].title() |
|
if self.conversation_state["device"] == "iphone": |
|
device_name = "iPhone" |
|
elif self.conversation_state["device"] == "mac": |
|
device_name = "Mac" |
|
response = f"I understand you need {issue_type} repair for your {device_name}. The estimated cost is {cost}. To find the nearest Apple Store, could you tell me your ZIP code or city?" |
|
else: |
|
response = "Could you describe the issue in more detail? For example, is it a cracked screen, battery problem, water damage, or something else?" |
|
|
|
elif current_step == "location_gathering": |
|
|
|
store_info = self.extract_location_info(user_input) |
|
if store_info: |
|
self.conversation_state["nearest_store"] = store_info |
|
self.conversation_state["step"] = "imei_gathering" |
|
response = f"Perfect! The nearest Apple Store is {store_info['name']} at {store_info['address']}. For the appointment, I'll need your device's IMEI or serial number. You can find this in Settings > General > About, or you can say 'skip' if you don't have it available." |
|
else: |
|
response = "I need your location to find the nearest Apple Store. Could you please provide your ZIP code or city name?" |
|
|
|
elif current_step == "imei_gathering": |
|
if "skip" in user_input.lower(): |
|
self.conversation_state["imei"] = "Will verify at appointment" |
|
else: |
|
imei = self.extract_imei(user_input) |
|
self.conversation_state["imei"] = imei if imei else "Will verify at appointment" |
|
|
|
self.conversation_state["step"] = "confirmation" |
|
device_name = self.conversation_state["device"].title() |
|
if self.conversation_state["device"] == "iphone": |
|
device_name = "iPhone" |
|
elif self.conversation_state["device"] == "mac": |
|
device_name = "Mac" |
|
|
|
response = f"""Let me confirm your repair appointment: |
|
|
|
π± Device: {device_name} |
|
π§ Issue: {self.conversation_state["issue"].title()} repair |
|
π° Estimated Cost: {self.conversation_state["estimated_cost"]} |
|
πͺ Location: {self.conversation_state["nearest_store"]["name"]} |
|
π Address: {self.conversation_state["nearest_store"]["address"]} |
|
π± IMEI: {self.conversation_state["imei"]} |
|
|
|
Should I proceed with booking this appointment? Say 'yes' to confirm or 'no' to start over.""" |
|
|
|
elif current_step == "confirmation": |
|
if any(word in user_input.lower() for word in ["yes", "confirm", "book", "schedule", "proceed"]): |
|
|
|
appointment = self.generate_appointment_details() |
|
self.conversation_state["step"] = "completed" |
|
|
|
device_name = self.conversation_state["device"].title() |
|
if self.conversation_state["device"] == "iphone": |
|
device_name = "iPhone" |
|
elif self.conversation_state["device"] == "mac": |
|
device_name = "Mac" |
|
|
|
response = f"""β
Appointment Successfully Booked! |
|
|
|
π« Confirmation Number: {appointment["confirmation"]} |
|
π
Date: {appointment["date"]} |
|
π Time: {appointment["time"]} |
|
πͺ Store: {self.conversation_state["nearest_store"]["name"]} |
|
π Store Phone: {self.conversation_state["nearest_store"]["phone"]} |
|
π Address: {self.conversation_state["nearest_store"]["address"]} |
|
|
|
π What to bring: |
|
β’ Government-issued photo ID |
|
β’ Your {device_name} |
|
β’ Proof of purchase (if available) |
|
|
|
β οΈ Before your appointment: |
|
β’ Back up your device |
|
β’ Turn off Find My iPhone (if applicable) |
|
β’ Remove any cases or screen protectors |
|
|
|
Your repair is scheduled! A confirmation has been sent to your Apple ID email. Is there anything else I can help you with today?""" |
|
elif any(word in user_input.lower() for word in ["no", "cancel", "start over"]): |
|
self.reset_conversation() |
|
response = "No problem! Let's start fresh. What device needs repair today?" |
|
else: |
|
response = "I need you to confirm the appointment. Please say 'yes' to proceed with booking or 'no' to start over." |
|
|
|
elif current_step == "completed": |
|
if any(word in user_input.lower() for word in ["thank", "thanks", "bye", "goodbye"]): |
|
response = "You're very welcome! Have a great day, and we'll see you at your appointment. If you need to reschedule or have questions, you can call the store directly or visit support.apple.com." |
|
elif any(word in user_input.lower() for word in ["new", "another", "different", "help"]): |
|
self.reset_conversation() |
|
response = "I'd be happy to help with another repair. What device needs assistance today?" |
|
else: |
|
response = "Your appointment is all set! Is there anything else I can help you with today, or would you like to schedule another repair?" |
|
|
|
|
|
self.conversation_state["history"].append({"role": "assistant", "content": response}) |
|
|
|
return response |
|
|
|
|
|
concierge = AppleCareVoiceConcierge() |
|
|
|
def process_transcribed_text(transcribed_text): |
|
"""Process transcribed text from speech input""" |
|
if not transcribed_text.strip(): |
|
return "I didn't catch that. Please try speaking again.", concierge.conversation_state["history"] |
|
|
|
response = concierge.process_conversation(transcribed_text) |
|
|
|
|
|
history = [] |
|
for msg in concierge.conversation_state["history"][-10:]: |
|
role = "π£οΈ You" if msg["role"] == "user" else "π€ Concierge" |
|
history.append(f"{role}: {msg['content']}") |
|
|
|
return response, "\n\n".join(history) |
|
|
|
def text_input_handler(text_input): |
|
"""Handle text input for testing""" |
|
if not text_input.strip(): |
|
return "Please enter a message.", concierge.conversation_state["history"] |
|
|
|
response = concierge.process_conversation(text_input) |
|
|
|
|
|
history = [] |
|
for msg in concierge.conversation_state["history"][-10:]: |
|
role = "π£οΈ You" if msg["role"] == "user" else "π€ Concierge" |
|
history.append(f"{role}: {msg['content']}") |
|
|
|
return response, "\n\n".join(history) |
|
|
|
def reset_conversation(): |
|
"""Reset the conversation""" |
|
concierge.reset_conversation() |
|
return "Conversation reset! Hello! I'm your AppleCare concierge. How can I help with your device today?", "" |
|
|
|
|
|
def create_interface(): |
|
with gr.Blocks( |
|
css=""" |
|
.gradio-container { |
|
max-width: 900px !important; |
|
margin: 0 auto !important; |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
} |
|
.main-container { |
|
background: linear-gradient(145deg, #1a1f35, #0d1226); |
|
border-radius: 24px; |
|
box-shadow: 20px 20px 60px #0a0e1d, -20px -20px 60px #20284d; |
|
padding: 30px; |
|
margin: 20px auto; |
|
border: 1px solid #ff7b25; |
|
} |
|
.title { |
|
text-align: center; |
|
color: #ff7b25; |
|
margin-bottom: 25px; |
|
font-weight: 800; |
|
font-size: 32px; |
|
text-shadow: 0 0 10px rgba(255, 123, 37, 0.5); |
|
} |
|
.subtitle { |
|
text-align: center; |
|
color: #64b5f6; |
|
margin-bottom: 30px; |
|
font-size: 18px; |
|
} |
|
.response-box { |
|
background: linear-gradient(145deg, #1e243b, #151a30); |
|
border-radius: 20px; |
|
box-shadow: inset 5px 5px 10px #0d111f, inset -5px -5px 10px #272f57; |
|
padding: 20px; |
|
margin: 15px 0; |
|
border: none; |
|
min-height: 200px; |
|
color: #ff7b25; |
|
font-weight: 500; |
|
} |
|
.conversation-history { |
|
background: linear-gradient(145deg, #1e243b, #151a30); |
|
border-radius: 20px; |
|
box-shadow: inset 5px 5px 10px #0d111f, inset -5px -5px 10px #272f57; |
|
padding: 20px; |
|
margin: 15px 0; |
|
border: none; |
|
min-height: 300px; |
|
max-height: 400px; |
|
overflow-y: auto; |
|
color: #64b5f6; |
|
} |
|
.btn-primary { |
|
background: linear-gradient(145deg, #ff7b25, #e55a00); |
|
border: none; |
|
border-radius: 16px; |
|
box-shadow: 5px 5px 10px #0d111f, -5px -5px 10px #272f57; |
|
color: #0d1226; |
|
padding: 12px 25px; |
|
margin: 8px; |
|
transition: all 0.3s ease; |
|
font-weight: 600; |
|
} |
|
.btn-primary:hover { |
|
box-shadow: 3px 3px 6px #0d111f, -3px -3px 6px #272f57; |
|
transform: translateY(2px); |
|
background: linear-gradient(145deg, #e55a00, #ff7b25); |
|
} |
|
.btn-secondary { |
|
background: linear-gradient(145deg, #64b5f6, #2196f3); |
|
border: none; |
|
border-radius: 16px; |
|
box-shadow: 5px 5px 10px #0d111f, -5px -5px 10px #272f57; |
|
color: #0d1226; |
|
padding: 12px 25px; |
|
margin: 8px; |
|
transition: all 0.3s ease; |
|
font-weight: 600; |
|
} |
|
.btn-secondary:hover { |
|
box-shadow: 3px 3px 6px #0d111f, -3px -3px 6px #272f57; |
|
transform: translateY(2px); |
|
background: linear-gradient(145deg, #2196f3, #64b5f6); |
|
} |
|
.btn-stop { |
|
background: linear-gradient(145deg, #f44336, #d32f2f); |
|
border: none; |
|
border-radius: 16px; |
|
box-shadow: 5px 5px 10px #0d111f, -5px -5px 10px #272f57; |
|
color: white; |
|
padding: 12px 25px; |
|
margin: 8px; |
|
transition: all 0.3s ease; |
|
font-weight: 600; |
|
} |
|
.btn-stop:hover { |
|
box-shadow: 3px 3px 6px #0d111f, -3px -3px 6px #272f57; |
|
transform: translateY(2px); |
|
background: linear-gradient(145deg, #d32f2f, #f44336); |
|
} |
|
.audio-input { |
|
border-radius: 16px; |
|
background: linear-gradient(145deg, #1e243b, #151a30); |
|
box-shadow: inset 5px 5px 10px #0d111f, inset -5px -5px 10px #272f57; |
|
padding: 15px; |
|
margin: 10px 0; |
|
border: 1px solid #ff7b25; |
|
} |
|
.text-input { |
|
border-radius: 16px; |
|
background: linear-gradient(145deg, #1e243b, #151a30); |
|
box-shadow: inset 5px 5px 10px #0d111f, inset -5px -5px 10px #272f57; |
|
padding: 15px; |
|
margin: 10px 0; |
|
border: 1px solid #64b5f6; |
|
color: #64b5f6; |
|
} |
|
.label { |
|
font-weight: 600; |
|
color: #ff7b25; |
|
margin-bottom: 8px; |
|
display: block; |
|
} |
|
.footer { |
|
text-align: center; |
|
margin-top: 25px; |
|
color: #64b5f6; |
|
font-size: 14px; |
|
} |
|
.status-indicator { |
|
text-align: center; |
|
margin: 10px 0; |
|
color: #ff7b25; |
|
font-style: italic; |
|
} |
|
/* Scrollbar styling */ |
|
.conversation-history::-webkit-scrollbar { |
|
width: 8px; |
|
} |
|
.conversation-history::-webkit-scrollbar-track { |
|
background: #151a30; |
|
border-radius: 4px; |
|
} |
|
.conversation-history::-webkit-scrollbar-thumb { |
|
background: #ff7b25; |
|
border-radius: 4px; |
|
} |
|
.conversation-history::-webkit-scrollbar-thumb:hover { |
|
background: #e55a00; |
|
} |
|
""" |
|
) as interface: |
|
|
|
|
|
gr.HTML(transformers_js_code) |
|
|
|
gr.HTML(""" |
|
<div class="title">π AppleCare Voice Concierge</div> |
|
<div class="subtitle">Your mystical AI-powered device repair assistant</div> |
|
""") |
|
|
|
with gr.Column(elem_classes="main-container"): |
|
|
|
gr.HTML(""" |
|
<div class="status-indicator" id="status-text"> |
|
Loading speech models... |
|
</div> |
|
""") |
|
|
|
|
|
with gr.Row(): |
|
audio_input = gr.Audio( |
|
sources=["microphone"], |
|
type="filepath", |
|
label="π€ Click to speak - I'm listening", |
|
elem_classes="audio-input" |
|
) |
|
|
|
|
|
hidden_transcript = gr.Textbox(visible=False, elem_id="hidden-transcript") |
|
|
|
|
|
with gr.Row(): |
|
text_input = gr.Textbox( |
|
placeholder="Or type your message here...", |
|
label="π¬ Text Input", |
|
lines=2, |
|
elem_classes="text-input" |
|
) |
|
|
|
|
|
with gr.Row(): |
|
submit_text_btn = gr.Button("π Send Text", elem_classes="btn-secondary") |
|
reset_btn = gr.Button("π Reset Conversation", elem_classes="btn-stop") |
|
speak_btn = gr.Button("π Speak Response", elem_classes="btn-primary") |
|
|
|
|
|
ai_response = gr.Textbox( |
|
label="π€ Concierge Response", |
|
value="Hello! I'm your AppleCare concierge. How can I help with your device today?", |
|
lines=8, |
|
interactive=False, |
|
elem_classes="response-box", |
|
elem_id="ai-response" |
|
) |
|
|
|
|
|
conversation_history = gr.Textbox( |
|
label="π Conversation History", |
|
lines=10, |
|
interactive=False, |
|
elem_classes="conversation-history" |
|
) |
|
|
|
gr.HTML(""" |
|
<div class="footer"> |
|
<p>This AI concierge uses Transformers.js for speech processing - no API keys required</p> |
|
<p>β
Check device issues β’ π Find nearby Apple Stores β’ π
Schedule appointments β’ π° Get repair estimates</p> |
|
</div> |
|
""") |
|
|
|
|
|
hidden_transcript.change( |
|
fn=process_transcribed_text, |
|
inputs=[hidden_transcript], |
|
outputs=[ai_response, conversation_history] |
|
) |
|
|
|
submit_text_btn.click( |
|
fn=text_input_handler, |
|
inputs=[text_input], |
|
outputs=[ai_response, conversation_history] |
|
) |
|
|
|
text_input.submit( |
|
fn=text_input_handler, |
|
inputs=[text_input], |
|
outputs=[ai_response, conversation_history] |
|
) |
|
|
|
reset_btn.click( |
|
fn=reset_conversation, |
|
outputs=[ai_response, conversation_history] |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
// Get the audio input element |
|
const audioInput = document.querySelector('input[type="file"]'); |
|
if (audioInput) { |
|
audioInput.addEventListener('change', function(event) { |
|
const file = event.target.files[0]; |
|
if (file) { |
|
// Handle the audio recording |
|
handleAudioRecording(file); |
|
} |
|
}); |
|
} |
|
|
|
// Add event listener for the speak button |
|
const speakButton = document.querySelector('.btn-primary'); |
|
if (speakButton) { |
|
speakButton.addEventListener('click', function() { |
|
const responseText = document.querySelector('#ai-response textarea').value; |
|
if (responseText) { |
|
speakText(responseText); |
|
} |
|
}); |
|
} |
|
}); |
|
</script> |
|
""") |
|
|
|
return interface |
|
|
|
|
|
if __name__ == "__main__": |
|
interface = create_interface() |
|
interface.launch( |
|
server_name="0.0.0.0", |
|
server_port=7860, |
|
share=False |
|
) |