├── README.md ├── app.py ├── requirements.txt ├── static ├── images │ ├── letters │ │ ├── a.jpg │ │ ├── b.jpg │ │ ├── c.jpg │ │ ├── d.jpg │ │ ├── e.jpg │ │ ├── f.jpg │ │ ├── g.jpg │ │ ├── h.jpg │ │ ├── l.jpg │ │ ├── m.jpg │ │ ├── o.jpg │ │ ├── p.jpg │ │ ├── s.jpg │ │ ├── t.jpg │ │ ├── u.jpg │ │ ├── æ.jpg │ │ ├── ð.jpg │ │ ├── ɔ.jpg │ │ ├── ə.jpg │ │ ├── ɛ.jpg │ │ ├── ɪ.jpg │ │ └── ʊ.jpg │ ├── loading.jpg │ └── santa.jpg ├── script.js └── style.css └── templates └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # Santa-GPT 2 | 3 | Talk to Santa Claus with your voice, and he answers with his voice. Try it out: https://santa.talking-gpt.com 4 | 5 | ## Quick Start 6 | 7 | ```console 8 | $ pip install -r requirements.txt 9 | $ python3 app.py 10 | $ google-chrome http://localhost:5000 11 | ``` 12 | 13 | ## API-keys 14 | 15 | You need to have your OpenAI API key in the `OPENAI_API_KEY` environment variable: 16 | 17 | ```console 18 | $ export OPENAI_API_KEY=YOUR_API_KEY 19 | ``` 20 | 21 | ## Text-to-Speech API 22 | 23 | By default, it uses OpenAI text-to-speech API for the voice generation. 24 | 25 | If you want to use ElevenLabs for text-to-speech, you also need to add your ElevenLabs API key to your environment and set the `SPEECH_API` environment variable to `elevenlabs`: 26 | 27 | ```console 28 | $ export ELEVEN_API_KEY=YOUR_API_KEY 29 | $ export SPEECH_API=elevenlabs 30 | ``` 31 | 32 | ## Changing the voice 33 | 34 | You can change the voice with the `VOICE_NAME` environment variable: 35 | 36 | ```console 37 | $ export VOICE_NAME=Oswald 38 | ``` 39 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | from elevenlabs import generate 3 | from pydub import AudioSegment 4 | from openai import OpenAI 5 | import eng_to_ipa as ipa 6 | import base64 7 | import uuid 8 | import io 9 | 10 | message_storage = {} 11 | 12 | app = Flask(__name__) 13 | client = OpenAI() 14 | 15 | def audio_duration(audio_bytes): 16 | audio = AudioSegment.from_file(io.BytesIO(audio_bytes)) 17 | return len(audio) 18 | 19 | @app.route("/") 20 | def index(): 21 | return render_template("index.html") 22 | 23 | @app.route("/message", methods=["POST"]) 24 | def message(): 25 | global message_storage 26 | 27 | message_history = [] 28 | session = request.json.get("session") or str(uuid.uuid4()) 29 | 30 | if session not in message_storage: 31 | message_storage[session] = [] 32 | 33 | message_history = message_storage[session] 34 | 35 | user_message = { 36 | "role": "user", 37 | "content": request.json["message"] 38 | } 39 | message_history.append(user_message) 40 | 41 | response = client.chat.completions.create( 42 | model="gpt-3.5-turbo", 43 | messages=[ 44 | { 45 | "role": "system", 46 | "content": "You are Santa Claus. Answer as Santa Claus talking to a child, in 1-5 sentences. Be funny.", 47 | } 48 | ] + message_history 49 | ) 50 | 51 | message = response.choices[0].message 52 | message_text = message.content 53 | 54 | message_history.append(message) 55 | 56 | if len(message_history) > 4: 57 | message_history.pop(0) 58 | 59 | if len(message_storage.keys()) > 50: 60 | message_storage = {} 61 | 62 | 63 | if os.getenv("SPEECH_API") == "elevenlabs": 64 | voice = os.getenv("VOICE_NAME") or "Santa Claus" 65 | audio = generate(message_text, voice=voice) 66 | else: 67 | voice = os.getenv("VOICE_NAME") or "onyx" 68 | audio = client.audio.speech.create( 69 | input=message_text, 70 | model="tts-1", 71 | voice=voice, 72 | ).read() 73 | 74 | audio_b64 = base64.b64encode(audio).decode("utf-8") 75 | 76 | return { 77 | "audio": audio_b64, 78 | "response": message_text, 79 | "ipa": ipa.convert(message_text), 80 | "duration": audio_duration(audio), 81 | "session": session, 82 | } 83 | 84 | if __name__ == "__main__": 85 | app.run(debug=True) 86 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | elevenlabs 2 | eng_to_ipa 3 | openai 4 | flask 5 | pydub 6 | -------------------------------------------------------------------------------- /static/images/letters/a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/a.jpg -------------------------------------------------------------------------------- /static/images/letters/b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/b.jpg -------------------------------------------------------------------------------- /static/images/letters/c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/c.jpg -------------------------------------------------------------------------------- /static/images/letters/d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/d.jpg -------------------------------------------------------------------------------- /static/images/letters/e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/e.jpg -------------------------------------------------------------------------------- /static/images/letters/f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/f.jpg -------------------------------------------------------------------------------- /static/images/letters/g.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/g.jpg -------------------------------------------------------------------------------- /static/images/letters/h.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/h.jpg -------------------------------------------------------------------------------- /static/images/letters/l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/l.jpg -------------------------------------------------------------------------------- /static/images/letters/m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/m.jpg -------------------------------------------------------------------------------- /static/images/letters/o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/o.jpg -------------------------------------------------------------------------------- /static/images/letters/p.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/p.jpg -------------------------------------------------------------------------------- /static/images/letters/s.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/s.jpg -------------------------------------------------------------------------------- /static/images/letters/t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/t.jpg -------------------------------------------------------------------------------- /static/images/letters/u.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/u.jpg -------------------------------------------------------------------------------- /static/images/letters/æ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/æ.jpg -------------------------------------------------------------------------------- /static/images/letters/ð.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/ð.jpg -------------------------------------------------------------------------------- /static/images/letters/ɔ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/ɔ.jpg -------------------------------------------------------------------------------- /static/images/letters/ə.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/ə.jpg -------------------------------------------------------------------------------- /static/images/letters/ɛ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/ɛ.jpg -------------------------------------------------------------------------------- /static/images/letters/ɪ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/ɪ.jpg -------------------------------------------------------------------------------- /static/images/letters/ʊ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/letters/ʊ.jpg -------------------------------------------------------------------------------- /static/images/loading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/loading.jpg -------------------------------------------------------------------------------- /static/images/santa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unconv/santa-gpt/7559e3c24bb6c0a2d984293400d09d554c23274e/static/images/santa.jpg -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | let initialized = false; 2 | let started = false; 3 | let session = null; 4 | let sending_message = false; 5 | 6 | try { 7 | var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition || null; 8 | } catch( e ) { 9 | document.querySelector("#start").textContent = "Sorry, your browser doesn't support speech recognition :("; 10 | } 11 | 12 | const recognition = new SpeechRecognition(); 13 | recognition.continuous = false; 14 | recognition.lang = 'en-US'; 15 | recognition.interimResults = false; 16 | recognition.maxAlternatives = 1; 17 | 18 | let animating = false; 19 | 20 | const all_letters = [ 21 | "æ", "d", "ɛ", "ɪ", "ɔ", "u", 22 | "a", "ð", "f", "l", "p", "ʊ", 23 | "b", "e", "g", "m", "s", 24 | "c", "ə", "h", "o", "t", 25 | ]; 26 | 27 | const loading = () => { 28 | document.querySelector("#loading").style.display = "block"; 29 | } 30 | 31 | const loaded = () => { 32 | document.querySelector("#loading").style.display = "none"; 33 | } 34 | 35 | const send_message = async (message) => { 36 | if( sending_message ) { 37 | return false; 38 | } 39 | 40 | sending_message = true; 41 | loading(); 42 | 43 | const response = await fetch("/message", { 44 | method: "POST", 45 | headers: { 46 | "Content-Type": "application/json" 47 | }, 48 | body: JSON.stringify({ 49 | "message": message, 50 | "session": session 51 | }) 52 | }); 53 | 54 | loaded(); 55 | 56 | const json = await response.json(); 57 | session = json.session; 58 | const duration = json.duration - 300; 59 | const audio_uri = "data:audio/wav;base64,"+json.audio; 60 | const letters = []; 61 | const stripped_response = json.ipa.toLowerCase().replace(/[\?!'ˈ ]/g, ''); 62 | 63 | for(let i = 0; i < stripped_response.length; i++) { 64 | let letter = stripped_response[i]; 65 | if( ! all_letters.includes(letter) ) { 66 | letter = all_letters[parseInt(Math.random()*all_letters.length)]; 67 | } 68 | letters.push(letter); 69 | } 70 | 71 | const animation_speed = duration / stripped_response.length; 72 | 73 | const audio = new Audio(audio_uri); 74 | setTimeout(() => { 75 | stop_animation(); 76 | sending_message = false; 77 | if( started ) { 78 | recognition.start(); 79 | } 80 | }, duration + animation_speed); 81 | 82 | start_animation(letters, animation_speed); 83 | audio.play(); 84 | } 85 | 86 | const start = () => { 87 | document.querySelector("#background").classList.remove("blurred"); 88 | document.querySelector("#start").style.display = "none"; 89 | document.querySelector("#stop").style.display = "block"; 90 | 91 | started = true; 92 | recognition.start(); 93 | 94 | if( ! initialized ) { 95 | initialized = true; 96 | 97 | recognition.onresult = function(event) { 98 | const message = event.results[0][0].transcript; 99 | 100 | send_message(message); 101 | } 102 | 103 | recognition.onspeechend = function() { 104 | recognition.stop(); 105 | } 106 | 107 | recognition.onerror = function(event) { 108 | console.error(event.error); 109 | } 110 | } 111 | }; 112 | 113 | const stop = () => { 114 | recognition.stop(); 115 | document.querySelector("#background").classList.add("blurred"); 116 | document.querySelector("#start").style.display = "block"; 117 | document.querySelector("#stop").style.display = "none"; 118 | } 119 | 120 | const next_frame = (letters, index, animation_speed) => { 121 | if( index > letters.length-1 ) { 122 | index = 0; 123 | } 124 | 125 | const bg_num1 = Number( index % 2 == 1 ); 126 | const bg_num2 = Number( index % 2 == 0 ); 127 | const background1 = "background" + bg_num1; 128 | const background2 = "background" + bg_num2; 129 | const letter = letters[index]; 130 | setTimeout(() => { 131 | if( ! animating ) { 132 | return; 133 | } 134 | 135 | const bg_element1 = document.querySelector("#"+background1); 136 | const bg_element2 = document.querySelector("#"+background2); 137 | bg_element2.style.backgroundImage = "url('/static/images/letters/"+letter+".jpg')"; 138 | bg_element2.style.zIndex = 2; 139 | bg_element1.style.zIndex = 1; 140 | 141 | next_frame(letters, index+1, animation_speed); 142 | }, animation_speed); 143 | } 144 | 145 | const start_animation = (letters, animation_speed) => { 146 | let index = 0; 147 | 148 | animating = true; 149 | next_frame(letters, index, animation_speed); 150 | } 151 | 152 | const stop_animation = () => { 153 | animating = false; 154 | document.querySelectorAll(".background").forEach( 155 | (e) => e.style.backgroundImage = "none" 156 | ); 157 | } 158 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | background-color: #000; 7 | } 8 | 9 | #background { 10 | background-image: url("/static/images/santa.jpg"); 11 | } 12 | 13 | #background, 14 | #background0, 15 | #background1, 16 | #loading { 17 | background-size: cover; 18 | background-position: 50% 50%; 19 | width: 100%; 20 | height: 100%; 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | } 25 | 26 | .blurred { 27 | filter: blur(15px); 28 | } 29 | 30 | #start { 31 | position: fixed; 32 | top: 50%; 33 | left: 50%; 34 | transform: translate(-50%, -50%); 35 | z-index: 4; 36 | font-size: 2em; 37 | padding: 15px; 38 | border-radius: 20px; 39 | background: #fff; 40 | border: 2px solid black; 41 | box-shadow: rgba(0, 0, 0, 0.8) 0 0 30px; 42 | cursor: pointer; 43 | } 44 | 45 | #stop { 46 | position: fixed; 47 | bottom: 20px; 48 | right: 20px; 49 | z-index: 4; 50 | font-size: 1.2em; 51 | padding: 12px; 52 | border-radius: 15px; 53 | background: #fff; 54 | border: 2px solid black; 55 | box-shadow: rgba(0, 0, 0, 0.8) 0 0 30px; 56 | cursor: pointer; 57 | display: none; 58 | } 59 | 60 | #loading { 61 | background-image: url("/static/images/loading.jpg"); 62 | display: none; 63 | z-index: 3; 64 | } 65 | 66 | @media (max-width: 500px) { 67 | #background, 68 | #background0, 69 | #background1, 70 | #loading { 71 | background-position: 30% 50%; 72 | } 73 | } -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Santa 8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------