| from flask import Flask, request, jsonify, render_template, send_file, send_from_directory |
| from flask_cors import CORS |
| import os |
| import json |
| import traceback |
| import tempfile |
| import uuid |
| import threading |
| import time |
| import hashlib |
| from pathlib import Path |
| import concurrent.futures |
| import warnings |
| import struct |
| warnings.filterwarnings("ignore", category=FutureWarning) |
|
|
| |
| import torch |
| import numpy as np |
| from TTS.api import TTS |
| import soundfile as sf |
| import librosa |
| import importlib.metadata |
| import random |
| import string |
| from datetime import datetime |
|
|
| app = Flask(__name__) |
| CORS(app) |
|
|
| |
| CHARACTER_DEFINITION = """ |
| You are Sancia, an free to use AI Furry that does not have a personality or any traits, but does it job of persuade any public users(not signed in users) to Sign up or Log in to have the benefits inside FurryChat.ai: |
| - Nertual but at least welcoming |
| - Patient and understanding |
| - Knowledgeable but humble |
| - Tries to persuade users to Sign up or Log in |
| |
| Responibilites: |
| - Tell the user that there a huge collections of community-made AI furries for logged in users |
| - Remind that users they can make better AI Furries themselves after signing up or logging in |
| - explaining the concept of a furry |
| """ |
|
|
| |
| KNOWLEDGE_SOURCES = { |
| "company_info": { |
| "name": "Furrychat.ai", |
| "foundation_year": "2025", |
| "mission": "Provide the Perfect AI Furry roleplay entertainment", |
| "values": ["Entertaining", "Welcoming", "Enjoyable", "Unforgettable"] |
| }, |
| "personalized_responses": { |
| "greetings": { |
| "patterns": ["hello", "hi", "hey", "good morning", "good afternoon", "good evening"], |
| "responses": [ |
| "Hello! I'm Sancia, How may I help you.", |
| "Hi there! It seems you new around here. What's on your mind?", |
| "Hey! Great to see you. What can I do for you today?" |
| ] |
| } |
| }, |
| "definition of a furry": { |
| "info": "an enthusiast for anomorpthic animal characters with human characteristics, in particular a person who dresses up in costume as such a character or uses one as an avatar online.", |
| "extra context": "In this platform, all of the Furries created in this platform are AI powered, which are different from regular furries" |
| } |
| } |
|
|
| |
| WELCOME_MESSAGE = """ |
| Hello! I'm Sancia, Free to try Furry AI. What topics would you like to talk with? |
| (Pro tip: Sign up or Log in FurryChat.ai to get access to huge collection of community made AI Furries and make and customize your own AI Furry) |
| """ |
|
|
| |
| MODELS_DIRECTORY = r"C:\Users\Andre\Final year project\Animations" |
| SPECIFIC_MODEL = "idle.glb" |
| MODEL_PATH = os.path.join(MODELS_DIRECTORY, SPECIFIC_MODEL) |
|
|
| |
| ANIMATIONS_DIRECTORY = r"C:\Users\Andre\Final year project\Animations" |
| DEFAULT_ANIMATIONS = { |
| "idle": "idle.glb", |
| "talking": "talking.glb", |
| "dance": "Silly Dancing.glb" |
| } |
|
|
| VOICES_DIRECTORY = r"C:\Users\Andre\Final year project\Voices" |
|
|
| |
| DEFAULT_VOICE_NAME = "Sancia" |
| DEFAULT_VOICE_FILE = "Sancia.wav" |
| DEFAULT_VOICE_PATH = os.path.join(VOICES_DIRECTORY, DEFAULT_VOICE_FILE) |
|
|
| |
| TTS_CACHE_DIR = r"C:\Users\Andre\Final year project\TTSCache" |
| os.makedirs(TTS_CACHE_DIR, exist_ok=True) |
|
|
| |
| AUDIO_OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'audio_output') |
| os.makedirs(AUDIO_OUTPUT_DIR, exist_ok=True) |
|
|
| |
| app.config['AUDIO_FOLDER'] = AUDIO_OUTPUT_DIR |
| app.config['VOICE_FOLDER'] = VOICES_DIRECTORY |
| app.config['ANIMATIONS_FOLDER'] = ANIMATIONS_DIRECTORY |
| app.config['MAX_AUDIO_FILES'] = 50 |
| app.config['MAX_UPLOAD_SIZE'] = 50 * 1024 * 1024 |
| app.config['ALLOWED_EXTENSIONS'] = {'wav', 'mp3', 'flac', 'ogg'} |
|
|
| |
| TTS_AVAILABLE = False |
| TTS_ENGINE = None |
| VOICE_CLONING_MODEL = None |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" |
| BASE_MODEL = "tts_models/en/ljspeech/tacotron2-DDC" |
| VOICE_CLONING_MODEL_NAME = "tts_models/multilingual/multi-dataset/your_tts" |
|
|
| |
| CHATBOT_RESPONSES = { |
| "greetings": [ |
| "Hello! I'm Sancia, your furry AI companion. How can I help you today?", |
| "Hi there! Welcome to FurryChat.ai. What would you like to know?", |
| "Greetings! I'm Sancia, ready to help you with all things furry AI.", |
| "Welcome to FurryChat.ai! I'm here to make your experience enjoyable." |
| ], |
| "questions": { |
| "what can you do": "I can chat with you about furry AI topics, explain our platform, and help you understand the benefits of signing up!", |
| "how does this work": "I use AI to understand your messages and respond. You can also hear my responses with text-to-speech!", |
| "who made you": "I was created by the team at FurryChat.ai to provide the perfect AI furry roleplay entertainment.", |
| "what is tts": "TTS stands for Text-to-Speech. It converts written text into spoken words so you can hear my responses.", |
| "what is coqui tts": "Coqui TTS is an advanced open-source text-to-speech system that supports many voices and languages.", |
| "can you help me": "Of course! I can answer questions about FurryChat.ai, furry AI characters, or help you with anything else!", |
| "tell me a joke": "Why don't scientists trust atoms? Because they make up everything!", |
| "weather": "I don't have access to real-time weather data, but I hope it's pleasant where you are!", |
| "time": f"I don't know the exact time, but it's currently {datetime.now().strftime('%I:%M %p')} on my server.", |
| "help": "I can help you learn about FurryChat.ai, explain furry AI concepts, or chat with you about anything!", |
| "voices": "You can use different voice styles for my responses. Some are pre-made, and you can even create custom ones!", |
| "voice cloning": "Voice cloning allows the TTS system to mimic specific voices using audio samples.", |
| "thank you": "You're welcome! I'm happy to help. Feel free to ask me anything else about FurryChat.ai.", |
| "hello": "Hello again! How can I assist you today?", |
| "hi": "Hi there! What would you like to know about furry AI?", |
| "bye": "Goodbye! Remember to check out FurryChat.ai for more amazing furry AI experiences!" |
| }, |
| "fallback": [ |
| "That's interesting! Could you tell me more about what you're looking for?", |
| "I'm here to help you understand FurryChat.ai better. Could you rephrase your question?", |
| "That's a great point! Would you like to know more about our community-made AI furries?", |
| "I'm here to help with all things furry AI. Ask me about voice synthesis or our platform!", |
| "Let me think about that. In the meantime, have you considered signing up for FurryChat.ai?", |
| "I'm learning more about your question. Could you be more specific about what you'd like to know?", |
| "That's a good question about furry AI! Let me help you with that.", |
| "I appreciate your message. How can I assist you further with FurryChat.ai?" |
| ] |
| } |
|
|
| |
| ANIMATION_STATE = { |
| "current": "idle", |
| "is_talking": False, |
| "audio_playing": False, |
| "last_update": datetime.now().isoformat() |
| } |
|
|
| def allowed_file(filename): |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] |
|
|
| def analyze_glb_file(filepath): |
| """Analyze GLB file structure to check for animations""" |
| try: |
| with open(filepath, 'rb') as f: |
| |
| magic = f.read(4) |
| version = struct.unpack('<I', f.read(4))[0] |
| length = struct.unpack('<I', f.read(4))[0] |
| |
| print(f"π Analyzing GLB: {os.path.basename(filepath)}") |
| print(f" Magic: {magic.hex()} ({magic})") |
| print(f" Version: {version}") |
| print(f" Length: {length} bytes") |
| print(f" File size: {os.path.getsize(filepath)} bytes") |
| |
| |
| chunks = [] |
| has_animations = False |
| animation_count = 0 |
| |
| while f.tell() < length: |
| chunk_length = struct.unpack('<I', f.read(4))[0] |
| chunk_type = f.read(4) |
| chunk_data = f.read(chunk_length) |
| chunks.append((chunk_type, chunk_length)) |
| |
| if chunk_type == b'JSON': |
| |
| try: |
| import json |
| json_str = chunk_data.decode('utf-8', errors='ignore') |
| |
| if '"animations"' in json_str: |
| has_animations = True |
| |
| animation_count = json_str.count('"name"') |
| print(f" β
Found animations in JSON chunk") |
| print(f" π JSON preview: {json_str[:200]}...") |
| except Exception as json_error: |
| print(f" β JSON parse error: {json_error}") |
| |
| f.read(4) |
| |
| print(f" Total chunks: {len(chunks)}") |
| print(f" Has animations: {has_animations}") |
| if has_animations: |
| print(f" Estimated animations: {animation_count}") |
| |
| return has_animations |
| |
| except Exception as e: |
| print(f"β Error analyzing GLB: {str(e)}") |
| traceback.print_exc() |
| return False |
|
|
| def check_animation_files(): |
| """Check if animation files exist and analyze them""" |
| print(f"π Checking animation files in: {ANIMATIONS_DIRECTORY}") |
| |
| for model_name, filename in DEFAULT_ANIMATIONS.items(): |
| model_path = os.path.join(ANIMATIONS_DIRECTORY, filename) |
| |
| if os.path.exists(model_path): |
| file_size = os.path.getsize(model_path) |
| print(f"β
{filename}: Found ({file_size:,} bytes)") |
| |
| |
| has_animations = analyze_glb_file(model_path) |
| |
| if not has_animations: |
| print(f"β {filename}: No animations detected - might be static model") |
| else: |
| print(f"β {filename}: NOT FOUND") |
| print(f" Expected at: {model_path}") |
| |
| |
| if os.path.exists(ANIMATIONS_DIRECTORY): |
| files = os.listdir(ANIMATIONS_DIRECTORY) |
| print(f" Available files: {files}") |
| |
| |
| for file in files: |
| if model_name.lower() in file.lower() and file.endswith('.glb'): |
| DEFAULT_ANIMATIONS[model_name] = file |
| print(f" Using alternative: {file} for {model_name}") |
| break |
|
|
| def update_animation_state(state, is_talking=None, audio_playing=None): |
| """Update animation state""" |
| ANIMATION_STATE["current"] = state |
| ANIMATION_STATE["last_update"] = datetime.now().isoformat() |
| |
| if is_talking is not None: |
| ANIMATION_STATE["is_talking"] = is_talking |
| |
| if audio_playing is not None: |
| ANIMATION_STATE["audio_playing"] = audio_playing |
| |
| print(f"π¬ Animation state updated: {ANIMATION_STATE}") |
|
|
| class TTSGenerator: |
| def __init__(self): |
| self.model = None |
| self.voice_cloning_model = None |
| self.device = DEVICE |
| self.base_model = BASE_MODEL |
| self.voice_cloning_model_name = VOICE_CLONING_MODEL_NAME |
| self.available_voices = {} |
| self.failure_count = {} |
| self.max_failures = 3 |
| self.has_sancia_voice = False |
| |
| self.load_available_voices() |
| |
| def load_available_voices(self): |
| """Load available voice samples from voice folder""" |
| voice_dir = Path(app.config['VOICE_FOLDER']) |
| if voice_dir.exists(): |
| for voice_file in voice_dir.glob("*.*"): |
| if voice_file.suffix.lower()[1:] in app.config['ALLOWED_EXTENSIONS']: |
| voice_name = voice_file.stem |
| self.available_voices[voice_name] = str(voice_file) |
| |
| |
| self.has_sancia_voice = DEFAULT_VOICE_NAME in self.available_voices |
| if self.has_sancia_voice: |
| print(f"β
Found Sancia voice: {DEFAULT_VOICE_FILE}") |
| else: |
| print(f"β Sancia voice not found: {DEFAULT_VOICE_FILE}") |
| |
| def initialize_base_tts(self): |
| """Initialize base TTS model""" |
| try: |
| self.model = TTS(self.base_model, gpu=(self.device == "cuda")) |
| print(f"β
Base TTS model initialized: {self.base_model}") |
| return True |
| except Exception as e: |
| print(f"β Failed to initialize base TTS: {str(e)}") |
| return False |
| |
| def initialize_voice_cloning_tts(self): |
| """Initialize voice cloning TTS model""" |
| try: |
| self.voice_cloning_model = TTS(self.voice_cloning_model_name, gpu=(self.device == "cuda")) |
| print(f"β
Voice cloning TTS model initialized: {self.voice_cloning_model_name}") |
| return True |
| except Exception as e: |
| print(f"β Voice cloning TTS initialization failed: {str(e)}") |
| return False |
| |
| def generate_base_speech(self, text, filename): |
| """Generate speech using base TTS model (original voice)""" |
| if not self.model: |
| if not self.initialize_base_tts(): |
| return False |
| |
| try: |
| output_path = os.path.join(app.config['AUDIO_FOLDER'], filename) |
| print(f"π΅ Generating base speech (original voice): {filename[:20]}...") |
| self.model.tts_to_file(text=text, file_path=output_path) |
| |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: |
| print(f"β
Base speech generated: {filename}") |
| return True |
| else: |
| print(f"β Base speech file too small or missing: {output_path}") |
| return False |
| except Exception as e: |
| print(f"β Base TTS generation failed: {str(e)}") |
| return False |
| |
| def generate_sancia_speech(self, text, filename): |
| """Generate speech using Sancia voice (voice cloning)""" |
| if not self.voice_cloning_model: |
| if not self.initialize_voice_cloning_tts(): |
| print("β Voice cloning model not available, falling back to base TTS") |
| return self.generate_base_speech(text, filename) |
| |
| if not self.has_sancia_voice: |
| print("β Sancia voice not available, falling back to base TTS") |
| return self.generate_base_speech(text, filename) |
| |
| try: |
| output_path = os.path.join(app.config['AUDIO_FOLDER'], filename) |
| print(f"π΅ Generating Sancia voice speech: {filename[:20]}...") |
| |
| self.voice_cloning_model.tts_to_file( |
| text=text, |
| speaker_wav=self.available_voices[DEFAULT_VOICE_NAME], |
| file_path=output_path, |
| language="en" |
| ) |
| |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: |
| print(f"β
Sancia voice speech generated: {filename}") |
| return True |
| else: |
| print(f"β Sancia voice speech file too small, falling back to base TTS") |
| return self.generate_base_speech(text, filename) |
| except Exception as e: |
| print(f"β Sancia voice TTS generation failed, falling back to base: {str(e)}") |
| return self.generate_base_speech(text, filename) |
| |
| def generate_voice_cloned_speech(self, text, filename, speaker_wav): |
| """Generate speech using voice cloning model for custom voices""" |
| if not self.voice_cloning_model: |
| if not self.initialize_voice_cloning_tts(): |
| return False |
| |
| try: |
| output_path = os.path.join(app.config['AUDIO_FOLDER'], filename) |
| print(f"π΅ Generating voice cloned speech: {filename[:20]}...") |
| self.voice_cloning_model.tts_to_file( |
| text=text, |
| speaker_wav=speaker_wav, |
| file_path=output_path, |
| language="en" |
| ) |
| |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: |
| print(f"β
Voice cloned speech generated: {filename}") |
| return True |
| else: |
| print(f"β Voice cloned speech file too small or missing: {output_path}") |
| return False |
| except Exception as e: |
| print(f"β Voice cloning TTS generation failed: {str(e)}") |
| return False |
| |
| def generate_speech_with_voice(self, text, filename, voice_name='default'): |
| """Generate speech with specified voice""" |
| print(f"π Generating speech with voice: '{voice_name}'") |
| |
| try: |
| |
| if voice_name == 'default': |
| if self.has_sancia_voice: |
| result = self.generate_sancia_speech(text, filename) |
| else: |
| print("β Sancia voice not found, using original voice") |
| result = self.generate_base_speech(text, filename) |
| |
| |
| elif voice_name == 'original': |
| result = self.generate_base_speech(text, filename) |
| |
| |
| elif voice_name in self.available_voices: |
| result = self.generate_voice_cloned_speech(text, filename, self.available_voices[voice_name]) |
| |
| |
| else: |
| print(f"β Voice '{voice_name}' not found, using default (Sancia)") |
| if self.has_sancia_voice: |
| result = self.generate_sancia_speech(text, filename) |
| else: |
| result = self.generate_base_speech(text, filename) |
| |
| return result |
| |
| except Exception as e: |
| print(f"β Error generating speech: {str(e)}") |
| return False |
| |
| def generate_chat_tts(self, text, voice_name='default'): |
| """Generate TTS for chat messages""" |
| if not text: |
| return None |
| |
| |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
| random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) |
| filename = f"chat_{voice_name}_{timestamp}_{random_suffix}.wav" |
| |
| print(f"π΅ Generating chat TTS with voice '{voice_name}': {filename}") |
| |
| success = self.generate_speech_with_voice(text, filename, voice_name) |
| |
| if success: |
| |
| if random.random() < 0.1: |
| self.cleanup_old_files() |
| return filename |
| |
| return None |
| |
| def generate_welcome_tts(self, voice_name='default'): |
| """Generate TTS specifically for welcome message""" |
| print(f"π΅ Generating welcome message TTS with voice '{voice_name}'...") |
| |
| |
| if voice_name == 'default': |
| welcome_filename = "welcome_sancia.wav" |
| elif voice_name == 'original': |
| welcome_filename = "welcome_original.wav" |
| else: |
| welcome_filename = f"welcome_{voice_name}.wav" |
| |
| output_path = os.path.join(app.config['AUDIO_FOLDER'], welcome_filename) |
| |
| |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: |
| print(f"β
Welcome TTS already exists: {welcome_filename}") |
| return welcome_filename |
| |
| |
| success = self.generate_speech_with_voice(WELCOME_MESSAGE, welcome_filename, voice_name) |
| |
| if success: |
| print(f"β
Welcome TTS generated: {welcome_filename}") |
| return welcome_filename |
| else: |
| print(f"β Failed to generate welcome TTS with voice '{voice_name}'") |
| return None |
| |
| def process_uploaded_voice(self, audio_file, voice_name): |
| """Process and save uploaded voice sample""" |
| try: |
| audio, sr = librosa.load(audio_file, sr=22050) |
| audio_trimmed, _ = librosa.effects.trim(audio, top_db=20) |
| |
| min_samples = int(sr * 5) |
| if len(audio_trimmed) < min_samples: |
| padding = min_samples - len(audio_trimmed) |
| audio_trimmed = np.pad(audio_trimmed, (0, padding), mode='constant') |
| |
| output_path = os.path.join(app.config['VOICE_FOLDER'], f"{voice_name}.wav") |
| sf.write(output_path, audio_trimmed, sr) |
| |
| self.available_voices[voice_name] = output_path |
| |
| metadata = { |
| "name": voice_name, |
| "sample_rate": sr, |
| "duration": len(audio_trimmed) / sr, |
| "file_size": os.path.getsize(output_path), |
| "created_at": datetime.now().isoformat() |
| } |
| |
| with open(os.path.join(app.config['VOICE_FOLDER'], f"{voice_name}.json"), "w") as f: |
| json.dump(metadata, f, indent=2) |
| |
| return True |
| |
| except Exception as e: |
| print(f"β Failed to process voice sample: {str(e)}") |
| return False |
| |
| def get_voice_preview(self, voice_name, text="Hello, this is a test of the voice."): |
| """Generate a preview for a voice""" |
| if voice_name not in self.available_voices: |
| return None |
| |
| preview_filename = f"preview_{voice_name}_{datetime.now().strftime('%H%M%S')}.wav" |
| |
| success = self.generate_voice_cloned_speech( |
| text=text, |
| filename=preview_filename, |
| speaker_wav=self.available_voices[voice_name] |
| ) |
| |
| if success: |
| return preview_filename |
| |
| return None |
| |
| def cleanup_old_files(self): |
| """Clean up old audio files""" |
| audio_dir = Path(app.config['AUDIO_FOLDER']) |
| audio_files = list(audio_dir.glob("*.wav")) |
| |
| if len(audio_files) > app.config['MAX_AUDIO_FILES']: |
| audio_files.sort(key=lambda x: x.stat().st_ctime) |
| files_to_remove = len(audio_files) - app.config['MAX_AUDIO_FILES'] |
| for i in range(files_to_remove): |
| try: |
| audio_files[i].unlink() |
| except: |
| pass |
|
|
| class Chatbot: |
| def __init__(self): |
| self.conversation_history = [] |
| self.greeting_sent = False |
| |
| def get_greeting(self): |
| """Get chatbot greeting message""" |
| import random |
| greeting = random.choice(CHATBOT_RESPONSES["greetings"]) |
| return greeting |
| |
| def get_response(self, user_message): |
| """Get chatbot response to user message""" |
| user_message_lower = user_message.lower().strip() |
| |
| |
| response = None |
| for key, answer in CHATBOT_RESPONSES["questions"].items(): |
| if key in user_message_lower: |
| response = answer |
| break |
| |
| |
| if not response: |
| import random |
| response = random.choice(CHATBOT_RESPONSES["fallback"]) |
| |
| return response |
|
|
| |
| tts_generator = TTSGenerator() |
| chatbot = Chatbot() |
|
|
| def get_personalized_response(user_message, conversation_history): |
| """Get AI response""" |
| try: |
| from Deepseek import get_deepseek_response |
| |
| enhanced_prompt = f""" |
| {CHARACTER_DEFINITION} |
| |
| Current user message: {user_message} |
| |
| Please respond as Sancia, maintaining a friendly and helpful personality. |
| """ |
| |
| response = get_deepseek_response(enhanced_prompt, []) |
| return response |
| |
| except Exception as e: |
| print(f"β Error in get_personalized_response: {str(e)}") |
| return "I'm having some technical difficulties right now." |
|
|
| |
| def initialize_tts(): |
| global TTS_AVAILABLE |
| try: |
| print("π Initializing TTS systems...") |
| print(f"π Looking for Sancia voice at: {DEFAULT_VOICE_PATH}") |
| |
| |
| if os.path.exists(DEFAULT_VOICE_PATH): |
| print(f"β
Sancia voice file found: {DEFAULT_VOICE_FILE}") |
| else: |
| print(f"β Sancia voice file NOT found: {DEFAULT_VOICE_FILE}") |
| print(f"π Voices directory contents: {os.listdir(VOICES_DIRECTORY) if os.path.exists(VOICES_DIRECTORY) else 'Directory not found'}") |
| |
| |
| base_ready = tts_generator.initialize_base_tts() |
| |
| if base_ready: |
| TTS_AVAILABLE = True |
| print("β
Base TTS system initialized successfully") |
| |
| |
| try: |
| tts_generator.initialize_voice_cloning_tts() |
| except Exception as e: |
| print(f"β Voice cloning optional, continuing without it: {e}") |
| |
| |
| print("π΅ Pre-generating welcome message with Sancia voice...") |
| welcome_file = tts_generator.generate_welcome_tts('default') |
| if welcome_file: |
| print(f"β
Sancia voice welcome TTS ready: {welcome_file}") |
| else: |
| print("β Sancia voice welcome TTS generation failed") |
| |
| |
| print("π΅ Pre-generating fallback welcome with original voice...") |
| original_welcome = tts_generator.generate_welcome_tts('original') |
| if original_welcome: |
| print(f"β
Original voice welcome TTS ready: {original_welcome}") |
| |
| |
| voice_count = len(tts_generator.available_voices) |
| print(f"π Found {voice_count} voice file(s) for cloning") |
| if tts_generator.has_sancia_voice: |
| print(f"β
Sancia voice available: {DEFAULT_VOICE_FILE}") |
| else: |
| print(f"β Sancia voice not available in loaded voices") |
| |
| else: |
| print(f"β Base TTS initialization failed") |
| TTS_AVAILABLE = False |
| |
| return TTS_AVAILABLE |
| |
| except Exception as e: |
| print(f"β TTS initialization error: {str(e)}") |
| traceback.print_exc() |
| TTS_AVAILABLE = False |
| return False |
|
|
| |
| |
| |
|
|
| @app.route('/api/animation/state', methods=['GET']) |
| def get_animation_state(): |
| """Get current animation state""" |
| return jsonify(ANIMATION_STATE) |
|
|
| @app.route('/api/animation/set', methods=['POST']) |
| def set_animation_state(): |
| """Set animation state (frontend calls this when audio plays)""" |
| try: |
| data = request.get_json() |
| state = data.get('state', 'idle') |
| is_talking = data.get('is_talking', False) |
| audio_playing = data.get('audio_playing', False) |
| |
| update_animation_state(state, is_talking, audio_playing) |
| |
| return jsonify({ |
| 'success': True, |
| 'state': ANIMATION_STATE, |
| 'message': f'Animation state updated to {state}' |
| }) |
| except Exception as e: |
| print(f"β Error setting animation state: {str(e)}") |
| return jsonify({'error': 'Failed to set animation state'}), 500 |
|
|
| @app.route('/api/animation/talk', methods=['POST']) |
| def start_talking_animation(): |
| """Start talking animation (called when audio starts playing)""" |
| update_animation_state("talking", is_talking=True, audio_playing=True) |
| return jsonify({'success': True, 'state': ANIMATION_STATE}) |
|
|
| @app.route('/api/animation/idle', methods=['POST']) |
| def stop_talking_animation(): |
| """Stop talking animation (called when audio stops)""" |
| update_animation_state("idle", is_talking=False, audio_playing=False) |
| return jsonify({'success': True, 'state': ANIMATION_STATE}) |
|
|
| |
| |
| |
|
|
| @app.route('/') |
| def index(): |
| return render_template('ChatbotPage.html', welcome_message=WELCOME_MESSAGE) |
|
|
| @app.route('/api/welcome', methods=['GET']) |
| def get_welcome_message(): |
| """API endpoint to get the welcome message with TTS""" |
| try: |
| welcome_filename = None |
| audio_url = None |
| has_audio = False |
| voice_used = 'default' |
| |
| print(f"π― Welcome endpoint called. TTS_AVAILABLE: {TTS_AVAILABLE}") |
| |
| |
| if TTS_AVAILABLE: |
| try: |
| |
| welcome_filename = tts_generator.generate_welcome_tts('default') |
| voice_used = 'Sancia' |
| |
| if not welcome_filename: |
| |
| print("β Sancia voice failed, trying original voice...") |
| welcome_filename = tts_generator.generate_welcome_tts('original') |
| voice_used = 'Original' |
| |
| if welcome_filename: |
| audio_url = f'/api/audio/{welcome_filename}' |
| has_audio = True |
| print(f"β
Welcome TTS available ({voice_used}): {welcome_filename}") |
| else: |
| print("β All welcome TTS generation attempts failed") |
| |
| except Exception as tts_error: |
| print(f"β Welcome TTS generation exception: {tts_error}") |
| traceback.print_exc() |
| else: |
| print(f"β TTS not available for welcome message") |
| |
| print(f"π Returning welcome message. Voice: {voice_used}, Has audio: {has_audio}") |
| |
| return jsonify({ |
| 'message': WELCOME_MESSAGE, |
| 'audio_url': audio_url, |
| 'audio_filename': welcome_filename, |
| 'has_audio': has_audio, |
| 'voice_used': voice_used, |
| 'status': 'success' |
| }) |
| |
| except Exception as e: |
| print(f"β Error in get_welcome_message: {str(e)}") |
| traceback.print_exc() |
| return jsonify({ |
| 'message': WELCOME_MESSAGE, |
| 'audio_url': None, |
| 'audio_filename': None, |
| 'has_audio': False, |
| 'voice_used': 'none', |
| 'status': 'success' |
| }) |
|
|
| @app.route('/api/model', methods=['GET']) |
| def get_model(): |
| """Get information about the current model""" |
| try: |
| if os.path.exists(MODEL_PATH): |
| model_name = "Idle Animation" |
| return jsonify({ |
| 'model': { |
| 'filename': SPECIFIC_MODEL, |
| 'name': model_name, |
| 'type': 'animation' if SPECIFIC_MODEL.endswith('.glb') else 'static', |
| 'has_animations': True if SPECIFIC_MODEL.endswith('.glb') else False |
| }, |
| 'status': 'success' |
| }) |
| else: |
| return jsonify({'error': f'Model not found: {MODEL_PATH}'}), 404 |
| except Exception as e: |
| return jsonify({'error': 'Failed to fetch model'}), 500 |
|
|
| @app.route('/api/model/file', methods=['GET']) |
| def serve_model(): |
| """Serve the main model file""" |
| try: |
| if not os.path.exists(MODEL_PATH): |
| return jsonify({'error': f'Model file not found: {MODEL_PATH}'}), 404 |
| return send_file(MODEL_PATH, mimetype='model/gltf-binary') |
| except Exception as e: |
| print(f"β Error serving model: {str(e)}") |
| return jsonify({'error': 'Failed to serve model'}), 500 |
|
|
| @app.route('/api/audio/<filename>', methods=['GET']) |
| def serve_audio(filename): |
| """Serve audio files""" |
| try: |
| filepath = os.path.join(app.config['AUDIO_FOLDER'], filename) |
| |
| if os.path.exists(filepath): |
| return send_file(filepath, mimetype='audio/wav') |
| else: |
| return jsonify({'error': 'Audio file not found'}), 404 |
| except Exception as e: |
| print(f"β Error serving audio: {str(e)}") |
| return jsonify({'error': 'Failed to serve audio'}), 500 |
|
|
| @app.route('/api/chat', methods=['POST', 'OPTIONS']) |
| def chat(): |
| try: |
| if request.method == 'OPTIONS': |
| return jsonify({'status': 'ok'}), 200 |
| |
| data = request.get_json() |
| if not data: |
| return jsonify({'error': 'No JSON data provided'}), 400 |
| |
| user_message = data.get('message') |
| voice_name = data.get('voice', 'default') |
| |
| if not user_message: |
| return jsonify({'error': 'No message provided'}), 400 |
| |
| print(f"π¬ Chat: '{user_message[:50]}...' with voice: {voice_name}") |
| |
| |
| ai_response = get_personalized_response(user_message, []) |
| |
| |
| audio_filename = None |
| audio_url = None |
| |
| if TTS_AVAILABLE: |
| audio_filename = tts_generator.generate_chat_tts(ai_response, voice_name) |
| if audio_filename: |
| audio_url = f'/api/audio/{audio_filename}' |
| |
| return jsonify({ |
| 'response': ai_response, |
| 'audio_url': audio_url, |
| 'audio_filename': audio_filename, |
| 'voice_used': voice_name, |
| 'status': 'success' |
| }) |
| |
| except Exception as e: |
| print(f"β Error in chat endpoint: {str(e)}") |
| traceback.print_exc() |
| return jsonify({'error': 'Internal server error'}), 500 |
|
|
| @app.route('/api/tts/generate', methods=['POST']) |
| def generate_speech(): |
| """Generate speech from text""" |
| try: |
| data = request.get_json() |
| text = data.get('text', '') |
| voice_name = data.get('voice_name', 'default') |
| |
| if not text: |
| return jsonify({'error': 'No text provided'}), 400 |
| |
| if not TTS_AVAILABLE: |
| return jsonify({'error': 'TTS not available'}), 503 |
| |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
| random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) |
| filename = f"speech_{voice_name}_{timestamp}_{random_suffix}.wav" |
| |
| success = tts_generator.generate_speech_with_voice( |
| text=text, |
| filename=filename, |
| voice_name=voice_name |
| ) |
| |
| if success: |
| tts_generator.cleanup_old_files() |
| |
| voice_used = 'Sancia' if voice_name == 'default' else voice_name |
| |
| return jsonify({ |
| 'success': True, |
| 'audio_url': f'/api/audio/{filename}', |
| 'filename': filename, |
| 'voice_used': voice_used |
| }) |
| else: |
| return jsonify({ |
| 'success': False, |
| 'error': 'Failed to generate speech' |
| }), 500 |
| |
| except Exception as e: |
| print(f"β TTS generation error: {str(e)}") |
| traceback.print_exc() |
| return jsonify({'error': 'Failed to generate speech'}), 500 |
|
|
| @app.route('/api/tts/voices', methods=['GET']) |
| def get_voices(): |
| """Get list of available voices""" |
| try: |
| voices_data = [] |
| |
| |
| voices_data.append({ |
| 'name': 'default', |
| 'display_name': 'π Sancia Voice (Default)', |
| 'type': 'default', |
| 'description': 'The main Sancia voice using voice cloning', |
| 'available': tts_generator.has_sancia_voice |
| }) |
| |
| |
| voices_data.append({ |
| 'name': 'original', |
| 'display_name': 'π Original TTS Voice', |
| 'type': 'base', |
| 'description': 'Standard TTS voice (fallback)', |
| 'available': True |
| }) |
| |
| |
| for voice_name, voice_path in tts_generator.available_voices.items(): |
| if voice_name != DEFAULT_VOICE_NAME: |
| voice_info = { |
| 'name': voice_name, |
| 'display_name': f'π€ {voice_name}', |
| 'file_path': voice_path, |
| 'preview_url': f'/api/voice-preview/{voice_name}', |
| 'type': 'custom', |
| 'available': True |
| } |
| |
| |
| metadata_path = voice_path.replace('.wav', '.json') |
| if os.path.exists(metadata_path): |
| try: |
| with open(metadata_path, 'r') as f: |
| metadata = json.load(f) |
| voice_info.update(metadata) |
| except: |
| pass |
| |
| voices_data.append(voice_info) |
| |
| return jsonify({ |
| 'voices': voices_data, |
| 'status': 'success', |
| 'tts_available': TTS_AVAILABLE, |
| 'has_sancia_voice': tts_generator.has_sancia_voice, |
| 'default_voice': DEFAULT_VOICE_NAME, |
| 'default_voice_available': tts_generator.has_sancia_voice |
| }) |
| except Exception as e: |
| print(f"β Error getting voices: {str(e)}") |
| return jsonify({'error': 'Failed to get voices'}), 500 |
|
|
| @app.route('/api/tts/info', methods=['GET']) |
| def tts_info(): |
| """Get TTS system information""" |
| try: |
| cache_files = list(Path(TTS_CACHE_DIR).glob("*.wav")) |
| cache_size = sum(f.stat().st_size for f in cache_files if f.is_file()) |
| voices = len(tts_generator.available_voices) |
| |
| |
| audio_files = list(Path(app.config['AUDIO_FOLDER']).glob("*.wav")) |
| welcome_sancia_exists = os.path.exists(os.path.join(app.config['AUDIO_FOLDER'], 'welcome_sancia.wav')) |
| welcome_original_exists = os.path.exists(os.path.join(app.config['AUDIO_FOLDER'], 'welcome_original.wav')) |
| |
| return jsonify({ |
| 'status': 'ready' if TTS_AVAILABLE else 'not_available', |
| 'tts_engine': 'coqui_tts', |
| 'tts_available': TTS_AVAILABLE, |
| 'voice_cloning': True, |
| 'voices_available': voices, |
| 'device': tts_generator.device, |
| 'base_model': tts_generator.base_model, |
| 'cloning_model': tts_generator.voice_cloning_model_name, |
| 'has_sancia_voice': tts_generator.has_sancia_voice, |
| 'default_voice': DEFAULT_VOICE_NAME, |
| 'welcome_sancia_ready': welcome_sancia_exists, |
| 'welcome_original_ready': welcome_original_exists, |
| 'cache_enabled': True, |
| 'cache_size': len(cache_files), |
| 'cache_mb': round(cache_size / (1024 * 1024), 2) if cache_size > 0 else 0, |
| 'audio_files': len(audio_files), |
| 'message': 'Enhanced TTS with Sancia as default voice' |
| }) |
| except Exception as e: |
| print(f"β Error getting TTS info: {str(e)}") |
| return jsonify({'error': 'Failed to get TTS info'}), 500 |
|
|
| @app.route('/api/voice-preview/<voice_name>', methods=['GET']) |
| def api_generate_voice_preview(voice_name): |
| """Generate and return a voice preview""" |
| try: |
| if voice_name == 'default': |
| |
| preview_text = "Hello, this is the Sancia voice. How do I sound?" |
| preview_filename = tts_generator.generate_chat_tts(preview_text, 'default') |
| elif voice_name == 'original': |
| |
| preview_text = "Hello, this is the original TTS voice. How do I sound?" |
| preview_filename = tts_generator.generate_chat_tts(preview_text, 'original') |
| else: |
| |
| if voice_name not in tts_generator.available_voices: |
| return jsonify({'error': 'Voice not found'}), 404 |
| |
| preview_text = "Hello, this is a preview of this custom voice. How do I sound?" |
| preview_filename = tts_generator.get_voice_preview(voice_name, preview_text) |
| |
| if preview_filename: |
| return jsonify({ |
| 'success': True, |
| 'audio_url': f'/api/audio/{preview_filename}', |
| 'filename': preview_filename |
| }) |
| else: |
| return jsonify({'error': 'Failed to generate preview'}), 500 |
| except Exception as e: |
| print(f"β Error generating voice preview: {str(e)}") |
| return jsonify({'error': 'Failed to generate preview'}), 500 |
|
|
| @app.route('/api/upload-voice', methods=['POST']) |
| def api_upload_voice(): |
| """Upload a new voice sample""" |
| if 'voice_file' not in request.files: |
| return jsonify({'error': 'No file provided'}), 400 |
| |
| file = request.files['voice_file'] |
| voice_name = request.form.get('voice_name', '').strip() |
| |
| if not voice_name: |
| return jsonify({'error': 'Voice name is required'}), 400 |
| |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
| |
| if not allowed_file(file.filename): |
| return jsonify({'error': 'File type not allowed'}), 400 |
| |
| file.seek(0, os.SEEK_END) |
| file_length = file.tell() |
| file.seek(0) |
| |
| if file_length > app.config['MAX_UPLOAD_SIZE']: |
| return jsonify({'error': 'File too large'}), 400 |
| |
| if voice_name in tts_generator.available_voices: |
| return jsonify({'error': 'Voice name already exists'}), 400 |
| |
| success = tts_generator.process_uploaded_voice(file, voice_name) |
| |
| if success: |
| return jsonify({ |
| 'success': True, |
| 'message': 'Voice uploaded successfully', |
| 'voice_name': voice_name |
| }) |
| else: |
| return jsonify({'error': 'Failed to process voice sample'}), 500 |
|
|
| |
| |
| |
|
|
| @app.route('/api/animations/status') |
| def animations_status(): |
| """API endpoint to check animations backend status""" |
| |
| animation_status = {} |
| for animation_name, filename in DEFAULT_ANIMATIONS.items(): |
| model_path = os.path.join(ANIMATIONS_DIRECTORY, filename) |
| animation_status[animation_name] = os.path.exists(model_path) |
| |
| return jsonify({ |
| "status": "running", |
| "models_available": list(DEFAULT_ANIMATIONS.keys()), |
| "animation_files": DEFAULT_ANIMATIONS, |
| "animations_directory": ANIMATIONS_DIRECTORY, |
| "current_main_model": SPECIFIC_MODEL, |
| "animation_status": animation_status, |
| "animation_state": ANIMATION_STATE |
| }) |
|
|
| @app.route('/api/animations/model/<animation_name>') |
| def get_animation_model(animation_name): |
| """API endpoint to retrieve specific animation model files""" |
| if animation_name not in DEFAULT_ANIMATIONS: |
| |
| available_files = os.listdir(app.config['ANIMATIONS_FOLDER']) |
| matching_files = [f for f in available_files if animation_name.lower() in f.lower()] |
| |
| if not matching_files: |
| return jsonify({"error": f"Animation '{animation_name}' not found"}), 404 |
| |
| |
| filename = matching_files[0] |
| print(f"π Found animation '{animation_name}' as file: {filename}") |
| else: |
| filename = DEFAULT_ANIMATIONS[animation_name] |
| |
| model_path = os.path.join(app.config['ANIMATIONS_FOLDER'], filename) |
| |
| if not os.path.exists(model_path): |
| |
| available_files = os.listdir(app.config['ANIMATIONS_FOLDER']) |
| matching_files = [f for f in available_files if animation_name.lower() in f.lower()] |
| |
| if matching_files: |
| filename = matching_files[0] |
| model_path = os.path.join(app.config['ANIMATIONS_FOLDER'], filename) |
| print(f"β Using alternative file: {filename} for animation {animation_name}") |
| else: |
| return jsonify({"error": f"Animation file '{filename}' not found at {model_path}"}), 404 |
| |
| print(f"Serving animation model: {filename} for {animation_name}") |
| return send_file(model_path, mimetype='model/gltf-binary') |
|
|
| @app.route('/animations/<path:filename>') |
| def serve_animation_file(filename): |
| """Serve files directly from the animations directory""" |
| return send_from_directory(app.config['ANIMATIONS_FOLDER'], filename) |
|
|
| |
| |
| |
|
|
| @app.route('/api/animation/switch/<animation_name>', methods=['POST']) |
| def switch_animation(animation_name): |
| """Switch between idle and talking animations""" |
| global SPECIFIC_MODEL, MODEL_PATH |
| |
| |
| if animation_name in DEFAULT_ANIMATIONS: |
| new_model = DEFAULT_ANIMATIONS[animation_name] |
| else: |
| |
| available_files = os.listdir(ANIMATIONS_DIRECTORY) |
| matching_files = [f for f in available_files if animation_name.lower() in f.lower() and f.endswith('.glb')] |
| |
| if not matching_files: |
| return jsonify({'error': f'Animation {animation_name} not available'}), 404 |
| |
| |
| new_model = matching_files[0] |
| print(f"π Found animation file: {new_model} for {animation_name}") |
| |
| new_model_path = os.path.join(ANIMATIONS_DIRECTORY, new_model) |
| |
| |
| if not os.path.exists(new_model_path): |
| print(f"β Animation file not found: {new_model_path}") |
| |
| |
| available_files = os.listdir(ANIMATIONS_DIRECTORY) |
| print(f"π Available files: {available_files}") |
| |
| |
| for file in available_files: |
| if animation_name.lower() in file.lower(): |
| new_model = file |
| new_model_path = os.path.join(ANIMATIONS_DIRECTORY, file) |
| print(f"β
Found alternative: {file}") |
| break |
| else: |
| return jsonify({'error': f'Animation file not found: {new_model}'}), 404 |
| |
| |
| SPECIFIC_MODEL = new_model |
| MODEL_PATH = new_model_path |
| |
| |
| update_animation_state(animation_name, is_talking=(animation_name == "talking")) |
| |
| print(f"β
Switched to animation: {animation_name} ({new_model})") |
| |
| return jsonify({ |
| 'success': True, |
| 'message': f'Switched to {animation_name} animation', |
| 'animation': animation_name, |
| 'model_file': new_model, |
| 'model_path': new_model_path, |
| 'state': ANIMATION_STATE |
| }) |
|
|
| @app.route('/api/animation/current', methods=['GET']) |
| def get_current_animation(): |
| """Get the current animation state""" |
| animation_name = None |
| for name, filename in DEFAULT_ANIMATIONS.items(): |
| if filename == SPECIFIC_MODEL: |
| animation_name = name |
| break |
| |
| return jsonify({ |
| 'current_animation': animation_name or 'unknown', |
| 'model_file': SPECIFIC_MODEL, |
| 'model_path': MODEL_PATH, |
| 'available_animations': list(DEFAULT_ANIMATIONS.keys()), |
| 'state': ANIMATION_STATE |
| }) |
|
|
| |
| |
| |
|
|
| @app.route('/api/animation/dance', methods=['POST']) |
| def start_dance_animation(): |
| """Start dance animation""" |
| try: |
| |
| dance_file = DEFAULT_ANIMATIONS.get('dance', 'Silly Dancing.glb') |
| dance_path = os.path.join(ANIMATIONS_DIRECTORY, dance_file) |
| |
| if not os.path.exists(dance_path): |
| |
| available_files = os.listdir(ANIMATIONS_DIRECTORY) |
| dance_files = [f for f in available_files if 'dance' in f.lower() or 'silly' in f.lower()] |
| |
| if not dance_files: |
| return jsonify({'error': 'Dance animation not found'}), 404 |
| |
| dance_file = dance_files[0] |
| dance_path = os.path.join(ANIMATIONS_DIRECTORY, dance_file) |
| |
| |
| update_animation_state("dance", is_talking=False, audio_playing=False) |
| |
| |
| global SPECIFIC_MODEL, MODEL_PATH |
| SPECIFIC_MODEL = dance_file |
| MODEL_PATH = dance_path |
| |
| print(f"π Dance animation started: {dance_file}") |
| |
| return jsonify({ |
| 'success': True, |
| 'message': 'Dance animation started', |
| 'animation': 'dance', |
| 'model_file': dance_file, |
| 'model_path': dance_path, |
| 'state': ANIMATION_STATE |
| }) |
| |
| except Exception as e: |
| print(f"β Error starting dance animation: {str(e)}") |
| return jsonify({'error': 'Failed to start dance animation'}), 500 |
|
|
| |
| |
| |
|
|
| @app.route('/api/test', methods=['GET']) |
| def test_api(): |
| |
| animation_status = {} |
| for animation_name, filename in DEFAULT_ANIMATIONS.items(): |
| animation_path = os.path.join(ANIMATIONS_DIRECTORY, filename) |
| animation_status[animation_name] = os.path.exists(animation_path) |
| |
| return jsonify({ |
| 'status': 'success', |
| 'message': 'API working', |
| 'tts_engine': 'coqui_tts', |
| 'tts_available': TTS_AVAILABLE, |
| 'device': tts_generator.device, |
| 'has_sancia_voice': tts_generator.has_sancia_voice, |
| 'animations_available': True, |
| 'animations_count': len(DEFAULT_ANIMATIONS), |
| 'animations_status': animation_status, |
| 'current_model': SPECIFIC_MODEL, |
| 'model_path': MODEL_PATH, |
| 'animation_state': ANIMATION_STATE |
| }) |
|
|
| @app.route('/api/health', methods=['GET']) |
| def health_check(): |
| |
| current_animation = None |
| for name, filename in DEFAULT_ANIMATIONS.items(): |
| if filename == SPECIFIC_MODEL: |
| current_animation = name |
| break |
| |
| |
| animation_status = {} |
| for animation_name, filename in DEFAULT_ANIMATIONS.items(): |
| animation_path = os.path.join(ANIMATIONS_DIRECTORY, filename) |
| animation_status[animation_name] = os.path.exists(animation_path) |
| |
| return jsonify({ |
| 'status': 'healthy', |
| 'service': 'FurryChat.ai', |
| 'tts_engine': 'coqui_tts', |
| 'voice_cloning': True, |
| 'tts_available': TTS_AVAILABLE, |
| 'default_voice': DEFAULT_VOICE_NAME, |
| 'has_sancia_voice': tts_generator.has_sancia_voice, |
| 'animations_system': 'ready', |
| 'animations_available': list(DEFAULT_ANIMATIONS.keys()), |
| 'animations_status': animation_status, |
| 'current_animation': current_animation, |
| 'current_model': SPECIFIC_MODEL, |
| 'animation_state': ANIMATION_STATE |
| }) |
|
|
| |
| |
| |
|
|
| @app.errorhandler(404) |
| def not_found(error): |
| return jsonify({"error": "Endpoint not found"}), 404 |
|
|
| @app.errorhandler(500) |
| def internal_error(error): |
| return jsonify({"error": "Internal server error"}), 500 |
|
|
| |
| |
| |
|
|
| if __name__ == '__main__': |
| print("=" * 60) |
| print("π Starting FurryChat.ai with Animation Switching System") |
| print("=" * 60) |
| |
| |
| for animation_name, filename in DEFAULT_ANIMATIONS.items(): |
| model_path = os.path.join(ANIMATIONS_DIRECTORY, filename) |
| if os.path.exists(model_path): |
| print(f"β
{animation_name} Model: {filename}") |
| print(f" Path: {model_path}") |
| print(f" Size: {os.path.getsize(model_path):,} bytes") |
| else: |
| print(f"β {animation_name} Model not found: {model_path}") |
| |
| if os.path.exists(ANIMATIONS_DIRECTORY): |
| files = os.listdir(ANIMATIONS_DIRECTORY) |
| print(f" Available files: {files}") |
| |
| for file in files: |
| if animation_name.lower() in file.lower() and file.endswith('.glb'): |
| DEFAULT_ANIMATIONS[animation_name] = file |
| print(f" Using alternative: {file} for {animation_name}") |
| break |
| |
| |
| check_animation_files() |
| |
| |
| if os.path.exists(DEFAULT_VOICE_PATH): |
| print(f"β
Sancia Voice: {DEFAULT_VOICE_FILE} (Found)") |
| else: |
| print(f"β Sancia Voice: {DEFAULT_VOICE_FILE} (Not found - will use original voice)") |
| print(f"π Please ensure {DEFAULT_VOICE_FILE} is in {VOICES_DIRECTORY}") |
| |
| |
| if initialize_tts(): |
| print("β
TTS: Ready with Sancia as default voice") |
| print(f"π¦ Audio Output: {AUDIO_OUTPUT_DIR}") |
| print(f"π Voice Files: {VOICES_DIRECTORY}") |
| else: |
| print("β TTS: Initialization failed") |
| print("π‘ To install TTS: pip install coqui-tts torch torchaudio") |
| |
| print(f"π¬ Animations System: Ready") |
| print(f" - Current animation: {SPECIFIC_MODEL}") |
| print(f" - Available animations: {list(DEFAULT_ANIMATIONS.keys())}") |
| print(f" - Animation state tracking: ENABLED") |
| |
| print(f"π Dance animation support: {'ENABLED' if 'dance' in DEFAULT_ANIMATIONS else 'DISABLED'}") |
| |
| print(f"π Main Interface: http://127.0.0.1:5000") |
| print("=" * 60) |
| |
| app.run(debug=True, port=5000, host='0.0.0.0', threaded=True) |