papayaga commited on
Commit
49a7070
·
1 Parent(s): 1f8e954

voice generation and first story working

Browse files
Files changed (10) hide show
  1. README.md +4 -2
  2. __test.py +36 -1
  3. adaptors/llm.py +16 -9
  4. adaptors/voice.py +11 -28
  5. data/stories.db +0 -0
  6. helpers/__init__.py +31 -10
  7. homeros.py +10 -3
  8. main.py +20 -8
  9. prompts/__init__.py +1 -0
  10. prompts/paraphraser.py +19 -0
README.md CHANGED
@@ -72,12 +72,14 @@ It puts the user in charge of a how the story is going to develop.
72
  - [x] Set up flow management
73
  - [x] Add SQlite DB and save stories
74
  - [x] GPT-4 story generation in a gradio interface
75
- - [ ] Do the evaluator (if it's time to end)
76
- - [ ] Inerchange text output for play.ht voice generation
77
  - [ ] Interchange text input for whisper
 
78
  - [ ] Dockerfile and deploy (including magic word for access control)
79
 
80
  ## Enhancements
81
 
82
  - [ ] Add option to download the full story as one .mp3
 
83
  - [ ] Add meta-moderator role to manage story ark better
 
72
  - [x] Set up flow management
73
  - [x] Add SQlite DB and save stories
74
  - [x] GPT-4 story generation in a gradio interface
75
+ - [x] Do the evaluator (if it's time to end)
76
+ - [x] Inerchange text output for play.ht voice generation
77
  - [ ] Interchange text input for whisper
78
+ - [ ] Clear input on submit
79
  - [ ] Dockerfile and deploy (including magic word for access control)
80
 
81
  ## Enhancements
82
 
83
  - [ ] Add option to download the full story as one .mp3
84
+ - [ ] Add option to download full story text
85
  - [ ] Add meta-moderator role to manage story ark better
__test.py CHANGED
@@ -5,9 +5,44 @@ from adaptors.db import get_story, get_all_stories
5
  from pprint import pprint
6
  import json
7
  from loguru import logger
 
8
 
9
  def all_s():
10
  for s in get_all_stories():
11
  pprint(s.to_dict())
12
 
13
- all_s()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from pprint import pprint
6
  import json
7
  from loguru import logger
8
+ from adaptors.voice import say_new
9
 
10
  def all_s():
11
  for s in get_all_stories():
12
  pprint(s.to_dict())
13
 
14
+
15
+ def say(text):
16
+ pprint(text)
17
+ return say_new(text)
18
+
19
+
20
+ long_text = '''
21
+ In a distant and rarely trodden corner of Middle Earth, there
22
+ resided an unlikely hero, known to the local forest critters by the '
23
+ rather unusual name, Slippersnail. Unlike the courageous Hobbits or '
24
+ the formidable Elves, Slippersnail was of a forgotten forest folk, '
25
+ a tiny creature, small as a sparrow with an oversized leaf as an '
26
+ umbrella and a snail shell for his home. You could hardly tell if
27
+ he was a gnome, a brownie or something else, for his kind was
28
+ scarcely remembered, even in the oldest songs of the Elves.
29
+
30
+ Slippersnail, barely tall enough to poke his head above the
31
+ bracken, lived an unassuming life, busily brewing his famous
32
+ dandelion tea and tending to his miniature garden filled with
33
+ colorful nocturnal glow-flowers. One morning, as Slippersnail was
34
+ pruning his glow-flowers, he found a peculiar golden leaf which
35
+ glittered so brightly it could outshine the glow-flowers by far.
36
+ Soon he discovered that the leaf had a magical property: it could
37
+ create light as bright as a day where the darkness had fallen.
38
+
39
+ The news of the magical leaf reached the ears of the Dark Lord in
40
+ the East. Coveting the leaf to manipulate it for his evil motives,
41
+ Sauron dispatched a band of his malevolent minions in search of
42
+ Slippersnail and his leaf.
43
+ Unaware of the danger, Slippersnail saw the shadow creeping over
44
+ his garden now. But, what do you think should our little hero do
45
+ next?
46
+ '''
47
+
48
+ say(long_text)
adaptors/llm.py CHANGED
@@ -4,12 +4,14 @@ an abstraction over GPT-4 for easy substitution later if needed
4
 
5
  import openai
6
  import os
 
7
 
8
  openai.api_key = os.getenv('OPENAI_KEY')
9
 
10
- #MODEL = 'gpt-4'
11
- MODEL = 'gpt-3.5-turbo'
12
 
 
13
  def answer(system_message, user_and_assistant_messages):
14
  messages = [{
15
  "role":"system",
@@ -18,11 +20,16 @@ def answer(system_message, user_and_assistant_messages):
18
 
19
  messages.extend(user_and_assistant_messages)
20
 
21
- chat_completion = openai.ChatCompletion.create(
22
- model=MODEL,
23
- messages=messages
24
- )
25
-
26
- output = chat_completion.choices[0].message.content
27
- return output
 
 
 
 
 
28
 
 
4
 
5
  import openai
6
  import os
7
+ from tenacity import retry, wait_random_exponential, stop_after_attempt
8
 
9
  openai.api_key = os.getenv('OPENAI_KEY')
10
 
11
+ MODEL = 'gpt-4'
12
+ #MODEL = 'gpt-3.5-turbo'
13
 
14
+ @retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))
15
  def answer(system_message, user_and_assistant_messages):
16
  messages = [{
17
  "role":"system",
 
20
 
21
  messages.extend(user_and_assistant_messages)
22
 
23
+ try:
24
+ chat_completion = openai.ChatCompletion.create(
25
+ model=MODEL,
26
+ messages=messages
27
+ )
28
+ output = chat_completion.choices[0].message.content
29
+ return output
30
+
31
+ except Exception as e:
32
+ print("Unable to generate ChatCompletion response")
33
+ print(f"Exception: {e}")
34
+ return e
35
 
adaptors/voice.py CHANGED
@@ -6,8 +6,10 @@ import requests
6
  import sseclient
7
  from loguru import logger
8
  import os
 
9
  from pprint import pprint
10
  import json
 
11
 
12
  url = "https://play.ht/api/v2/tts"
13
  user_id = os.environ["PLAYHT_USERID"]
@@ -20,33 +22,12 @@ headers = {
20
  "X-USER-ID": user_id
21
  }
22
 
23
- voices = ["dylan"]
24
-
25
- #TODO pre-generate these with dylan and save locally
26
- fixed_sayings = {
27
- "welcome": [""], #also ask magic word here
28
- "wrong_magic_word": [""],
29
- "let_me_ask_questions": [""],
30
- "ask_world": [""],
31
- "ask_hero": [""],
32
- "ask_plot": [""],
33
- "ask_ending": [""],
34
- "ask_style": [""],
35
- "its_the_end": [""],
36
- "no_more_story": [""]
37
- }
38
-
39
- '''
40
- return an old file from a dictionary of pre-generated sayings
41
- '''
42
- def say_fixed(fixed_msg, voice):
43
- #We need to keep a repository of fixed messages generated in our key voices and for each have a file URL
44
- return
45
 
46
  '''
47
  generate new saying with play.ht
48
  '''
49
- def say_new(text, voice):
50
  payload = {
51
  "quality": "medium",
52
  "output_format": "mp3",
@@ -59,13 +40,15 @@ def say_new(text, voice):
59
  response = requests.post(url, stream=True, headers=headers, json=payload)
60
 
61
  stream_url = response.headers["content-location"]
62
- logger.debug(f"stream_url = {stream_url}")
63
 
64
  resp = requests.get(stream_url, stream=True, headers=headers)
65
 
66
  client = sseclient.SSEClient(resp)
67
  for event in client.events():
68
- if event.data:
69
- e = json.loads(event.data)
70
- if e["stage"] == "complete":
71
- return(e["url"])
 
 
 
 
6
  import sseclient
7
  from loguru import logger
8
  import os
9
+ import helpers
10
  from pprint import pprint
11
  import json
12
+ from tenacity import retry, wait_random_exponential, stop_after_attempt
13
 
14
  url = "https://play.ht/api/v2/tts"
15
  user_id = os.environ["PLAYHT_USERID"]
 
22
  "X-USER-ID": user_id
23
  }
24
 
25
+ valid_voices = ["dylan"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  '''
28
  generate new saying with play.ht
29
  '''
30
+ def say_new(text, voice="dylan"):
31
  payload = {
32
  "quality": "medium",
33
  "output_format": "mp3",
 
40
  response = requests.post(url, stream=True, headers=headers, json=payload)
41
 
42
  stream_url = response.headers["content-location"]
 
43
 
44
  resp = requests.get(stream_url, stream=True, headers=headers)
45
 
46
  client = sseclient.SSEClient(resp)
47
  for event in client.events():
48
+ if event.data:
49
+ #pprint(event.data)
50
+ if helpers.is_valid_json(event.data): # play.ht api is unrealiable
51
+ e = json.loads(event.data)
52
+ if e["stage"] == "complete":
53
+ return(e["url"])
54
+
data/stories.db CHANGED
Binary files a/data/stories.db and b/data/stories.db differ
 
helpers/__init__.py CHANGED
@@ -1,9 +1,24 @@
1
  import uuid
2
  import os
3
  import random
 
 
 
 
 
4
 
5
  magic_w = os.environ["MAGICWORD"]
6
 
 
 
 
 
 
 
 
 
 
 
7
  def gen_unique_id():
8
  return str(uuid.uuid4())
9
 
@@ -13,20 +28,26 @@ def check_magic_word(w):
13
  def get_fixed_msg(msg_type):
14
 
15
  fixed_sayings = {
16
- "welcome": ["Welcome! What's the magic word?"], #also ask magic word here
17
- "wrong_magic_word": ["Magic word is wrong. Try again."],
18
- "ask_world": ["Let me ask you some questions first. What kind of world should your story unfold in?"],
19
- "ask_hero": ["Who should the hero be?"],
20
- "ask_plot": ["Can you describe the plot in a few words?"],
21
- "ask_ending": ["What kind of ending would you like? A happy one? A tragic one? Something else?"],
22
- "ask_style": ["What kind of storytelling style do you wnat? Funny? Poetic?"],
23
- "its_the_end": ["And this is the end of our story. Thank you for listening."],
24
- "no_more_story": ["I'm sorry, this story has ended. Reload the page to do another story."]
25
  }
26
 
27
  if msg_type in fixed_sayings:
28
  saying = random.choice(fixed_sayings[msg_type])
29
- return saying
 
 
 
 
 
 
30
 
31
  else:
32
  raise Exception(f"fixed saying with msg_type {msg_type} not found")
 
1
  import uuid
2
  import os
3
  import random
4
+ import prompts
5
+ import json
6
+ from adaptors.llm import answer
7
+
8
+ from adaptors.voice import say_new
9
 
10
  magic_w = os.environ["MAGICWORD"]
11
 
12
+ '''
13
+ check if valid JSON
14
+ '''
15
+ def is_valid_json(input_string):
16
+ try:
17
+ json_object = json.loads(input_string)
18
+ except json.JSONDecodeError:
19
+ return False
20
+ return True
21
+
22
  def gen_unique_id():
23
  return str(uuid.uuid4())
24
 
 
28
  def get_fixed_msg(msg_type):
29
 
30
  fixed_sayings = {
31
+ "welcome": ["Welcome, welcome my friend! I'd be happy to tell you a story. But one thing first... Do you know the magic word?"],
32
+ "wrong_magic_word": ["Oh, my dear, I'm very sorry. But it looks like you don't know the magic word. And I'm afraid I can't tell you the story without the magic word. Please ask around for the magic word and try again then. I'll wait here."],
33
+ "ask_world": ["Wonderful! That is right... Get ready for a great story that you and I can create together. But before we start, let me ask you a few questions first. What kind of world would you like our story to unfold in? Maybe Middle Earth with elves and orcs? Or a distant future world full of space travel? It can be anything. Just tell me."],
34
+ "ask_hero": ["That sounds great. And who should our hero be?"],
35
+ "ask_plot": ["Splendid! Now let's think about the plot of our story. Can you describe in just a few words what our story should be all about. Maybe the hero finds the love of their life? Or maybe they travel far and wide to discover their powers and find friends... it can be anything you want. Just say it and we will make it happen."],
36
+ "ask_ending": ["That is great. And what kind of ending would you like our story to have? A happy one? A tragic one? Something else?"],
37
+ "ask_style": ["I see. Now lastly, what kind of storytelling style do you like most? Would you like our story to be funny? Or Epic? Or Poetic? You can decide!"],
38
+ "its_the_end": ["And this is the end of our story. Thank you, my dear, for making it with me."],
39
+ "no_more_story": ["I'm very sorry, my dear, but this story has ended. But you can come back again later and we will make another great story together."]
40
  }
41
 
42
  if msg_type in fixed_sayings:
43
  saying = random.choice(fixed_sayings[msg_type])
44
+ system_message = prompts.get('paraphraser')
45
+ paraphrased = answer(system_message, [{
46
+ "role" : "user",
47
+ "content" : saying
48
+ }])
49
+ audio = say_new(paraphrased)
50
+ return audio
51
 
52
  else:
53
  raise Exception(f"fixed saying with msg_type {msg_type} not found")
homeros.py CHANGED
@@ -6,8 +6,9 @@ from pprint import pprint
6
  from helpers import gen_unique_id
7
  import prompts
8
  from adaptors.llm import answer
 
9
 
10
- MAX_STORY_LEN = 2 #after how many chunks we force the story to end
11
 
12
  '''
13
  initiates a new story and saves in DB
@@ -52,6 +53,10 @@ def continue_story(user_input, story_data):
52
  "content": user_input
53
  })
54
  next_chunk_text = create_next_chunk_text(user_input, story)
 
 
 
 
55
  next_chunk_audio = create_next_chunk_audio(next_chunk_text)
56
  messages.append({
57
  "role":"assistant",
@@ -61,6 +66,8 @@ def continue_story(user_input, story_data):
61
  "text" : next_chunk_text,
62
  "audio_url" : next_chunk_audio
63
  })
 
 
64
  story.chunks = json.dumps(chunks)
65
  story.messages = json.dumps(messages)
66
  story.status = "ongoing"
@@ -135,7 +142,7 @@ def evaluate_story(story):
135
  evaluation = {}
136
  story_len = len(story["chunks"])
137
  logger.debug(story_len)
138
- evaluation["is_time_to_end"] = story_len > MAX_STORY_LEN
139
 
140
  return evaluation
141
 
@@ -144,4 +151,4 @@ def evaluate_story(story):
144
  turns next story chunk into audio and returns a URL
145
  '''
146
  def create_next_chunk_audio(text):
147
- return "url.com"
 
6
  from helpers import gen_unique_id
7
  import prompts
8
  from adaptors.llm import answer
9
+ from adaptors.voice import say_new
10
 
11
+ MAX_STORY_LEN = 3 #after how many chunks we force the story to end
12
 
13
  '''
14
  initiates a new story and saves in DB
 
53
  "content": user_input
54
  })
55
  next_chunk_text = create_next_chunk_text(user_input, story)
56
+
57
+ if len(chunks) == 0:
58
+ next_chunk_text = "May our story begin!\n\n"+next_chunk_text
59
+
60
  next_chunk_audio = create_next_chunk_audio(next_chunk_text)
61
  messages.append({
62
  "role":"assistant",
 
66
  "text" : next_chunk_text,
67
  "audio_url" : next_chunk_audio
68
  })
69
+ pprint(chunks)
70
+ pprint(messages)
71
  story.chunks = json.dumps(chunks)
72
  story.messages = json.dumps(messages)
73
  story.status = "ongoing"
 
142
  evaluation = {}
143
  story_len = len(story["chunks"])
144
  logger.debug(story_len)
145
+ evaluation["is_time_to_end"] = story_len >= MAX_STORY_LEN
146
 
147
  return evaluation
148
 
 
151
  turns next story chunk into audio and returns a URL
152
  '''
153
  def create_next_chunk_audio(text):
154
+ return say_new(text)
main.py CHANGED
@@ -8,12 +8,25 @@ import helpers
8
 
9
  from homeros import init_story, start_story, continue_story, finish_story, define_metadata, evaluate_story
10
 
 
 
11
  '''
12
  Here we manage the flow and state of the story
13
  '''
14
  def do_homeros(user_input, story):
15
 
16
- pprint(story)
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  # story hasn't started
19
  if story["status"] == "not_started":
@@ -67,7 +80,7 @@ def do_homeros(user_input, story):
67
  story = define_metadata(user_input, "style", story)
68
  story["status"] = "ongoing"
69
  story = start_story(story)
70
- next_message = story["chunks"][-1]["text"]
71
 
72
  # we are in the middle of the story - evaluate if time to end, or continue
73
  elif story["status"] == "ongoing":
@@ -79,7 +92,7 @@ def do_homeros(user_input, story):
79
  story = continue_story(user_input, story)
80
  story["status"] = "ongoing"
81
 
82
- next_message = story["chunks"][-1]["text"]
83
 
84
  # story has ended, but the user still inputting. tell them it's over
85
  elif story["status"] == "finished":
@@ -113,14 +126,12 @@ with demo:
113
  "full_story_text": ""
114
  })
115
 
116
- pprint(story.value)
117
-
118
  with gr.Row():
119
  gr.Markdown('''
120
  # HOMEROS
121
 
122
  This demo is exploring the future of interactive storytelling.
123
- It puts the user in charge and makes blurs the boundary between the reader and the author.
124
 
125
  Hit "Tell me!" to get started.
126
 
@@ -135,8 +146,9 @@ When Homeros asks you something - hit record, answer with your voice and then hi
135
  )
136
 
137
  with gr.Row():
138
- story_chunk = gr.Textbox(
139
- label="storyteller says"
 
140
  )
141
 
142
  with gr.Row():
 
8
 
9
  from homeros import init_story, start_story, continue_story, finish_story, define_metadata, evaluate_story
10
 
11
+ DEFAULT_PARAMS = True
12
+
13
  '''
14
  Here we manage the flow and state of the story
15
  '''
16
  def do_homeros(user_input, story):
17
 
18
+ # if default params is true - skip the asking, including magic word and just start the story
19
+ if DEFAULT_PARAMS and len(story["chunks"]) == 0:
20
+ story = init_story(story)
21
+ story = define_metadata("J.R.R. Tolkien's Middle Earth", "world", story)
22
+ story = define_metadata("I don't know. Please choose something unusual.", "hero", story)
23
+ story = define_metadata("I don't know. Please choose something unusual.", "plot", story)
24
+ story = define_metadata("Happy", "ending", story)
25
+ story = define_metadata("epic", "style", story)
26
+ story["status"] = "ongoing"
27
+ story = start_story(story)
28
+ next_message = story["chunks"][-1]["audio_url"]
29
+ return next_message, story
30
 
31
  # story hasn't started
32
  if story["status"] == "not_started":
 
80
  story = define_metadata(user_input, "style", story)
81
  story["status"] = "ongoing"
82
  story = start_story(story)
83
+ next_message = story["chunks"][-1]["audio_url"]
84
 
85
  # we are in the middle of the story - evaluate if time to end, or continue
86
  elif story["status"] == "ongoing":
 
92
  story = continue_story(user_input, story)
93
  story["status"] = "ongoing"
94
 
95
+ next_message = story["chunks"][-1]["audio_url"]
96
 
97
  # story has ended, but the user still inputting. tell them it's over
98
  elif story["status"] == "finished":
 
126
  "full_story_text": ""
127
  })
128
 
 
 
129
  with gr.Row():
130
  gr.Markdown('''
131
  # HOMEROS
132
 
133
  This demo is exploring the future of interactive storytelling.
134
+ It puts the user in charge and blurs the boundary between the reader and the author.
135
 
136
  Hit "Tell me!" to get started.
137
 
 
146
  )
147
 
148
  with gr.Row():
149
+ story_chunk = gr.Audio(
150
+ label="storyteller says",
151
+ autoplay=True
152
  )
153
 
154
  with gr.Row():
prompts/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
  from pprint import pprint
2
  from . import storyteller_general
 
3
 
4
  '''
5
  basic function that substitues variables in the prompt template and returns a ready prompt
 
1
  from pprint import pprint
2
  from . import storyteller_general
3
+ from . import paraphraser
4
 
5
  '''
6
  basic function that substitues variables in the prompt template and returns a ready prompt
prompts/paraphraser.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from string import Template
2
+
3
+ '''
4
+ paraphraser prompt
5
+ '''
6
+
7
+ template = Template('''
8
+
9
+ You are a system that helps paraphrase questions and sayings slightly, without changing style or key message. Your paraphrasing should be minimal. Occasionally you can even return unchanged messages.
10
+
11
+ The words and terms that shouldn't be changed under any circumstances and should remain in your paraphrased version unchanged:
12
+ - magic word
13
+ - hero
14
+ - plot
15
+ - world
16
+
17
+ The user gives you text and you return back a paraphrased version of the same text. Only return the paraphrased vesion and nothing else.
18
+
19
+ ''')