├── requirements.txt
├── static
├── images
│ ├── santa.jpg
│ ├── loading.jpg
│ └── 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
├── style.css
└── script.js
├── templates
└── index.html
├── README.md
└── app.py
/requirements.txt:
--------------------------------------------------------------------------------
1 | elevenlabs
2 | eng_to_ipa
3 | openai
4 | flask
5 | pydub
6 |
--------------------------------------------------------------------------------
/static/images/santa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/santa.jpg
--------------------------------------------------------------------------------
/static/images/loading.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/loading.jpg
--------------------------------------------------------------------------------
/static/images/letters/a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/a.jpg
--------------------------------------------------------------------------------
/static/images/letters/b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/b.jpg
--------------------------------------------------------------------------------
/static/images/letters/c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/c.jpg
--------------------------------------------------------------------------------
/static/images/letters/d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/d.jpg
--------------------------------------------------------------------------------
/static/images/letters/e.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/e.jpg
--------------------------------------------------------------------------------
/static/images/letters/f.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/f.jpg
--------------------------------------------------------------------------------
/static/images/letters/g.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/g.jpg
--------------------------------------------------------------------------------
/static/images/letters/h.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/h.jpg
--------------------------------------------------------------------------------
/static/images/letters/l.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/l.jpg
--------------------------------------------------------------------------------
/static/images/letters/m.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/m.jpg
--------------------------------------------------------------------------------
/static/images/letters/o.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/o.jpg
--------------------------------------------------------------------------------
/static/images/letters/p.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/p.jpg
--------------------------------------------------------------------------------
/static/images/letters/s.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/s.jpg
--------------------------------------------------------------------------------
/static/images/letters/t.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/t.jpg
--------------------------------------------------------------------------------
/static/images/letters/u.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/u.jpg
--------------------------------------------------------------------------------
/static/images/letters/æ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/æ.jpg
--------------------------------------------------------------------------------
/static/images/letters/ð.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/ð.jpg
--------------------------------------------------------------------------------
/static/images/letters/ɔ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/ɔ.jpg
--------------------------------------------------------------------------------
/static/images/letters/ə.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/ə.jpg
--------------------------------------------------------------------------------
/static/images/letters/ɛ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/ɛ.jpg
--------------------------------------------------------------------------------
/static/images/letters/ɪ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/ɪ.jpg
--------------------------------------------------------------------------------
/static/images/letters/ʊ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unconv/santa-gpt/HEAD/static/images/letters/ʊ.jpg
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Santa
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------