GJDutils 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gjdutils/__init__.py +12 -0
- gjdutils/audios.py +39 -0
- gjdutils/cacheing.py +237 -0
- gjdutils/cmd.py +149 -0
- gjdutils/colab.py +39 -0
- gjdutils/collections.py +36 -0
- gjdutils/decorators.py +34 -0
- gjdutils/dicts.py +216 -0
- gjdutils/dsci.py +202 -0
- gjdutils/dt.py +296 -0
- gjdutils/env.py +64 -0
- gjdutils/errors.py +12 -0
- gjdutils/files.py +140 -0
- gjdutils/functions.py +6 -0
- gjdutils/google_translate.py +80 -0
- gjdutils/hashing.py +32 -0
- gjdutils/html.py +87 -0
- gjdutils/indexing.py +97 -0
- gjdutils/iterfunc.py +99 -0
- gjdutils/jsons.py +70 -0
- gjdutils/lists.py +13 -0
- gjdutils/llm_utils.py +167 -0
- gjdutils/llms_claude.py +131 -0
- gjdutils/llms_openai.py +299 -0
- gjdutils/misc.py +30 -0
- gjdutils/num.py +77 -0
- gjdutils/obsolete/google_text_to_speech.py +46 -0
- gjdutils/obsolete/llms_obsolete.py +298 -0
- gjdutils/outloud_text_to_speech.py +230 -0
- gjdutils/prompt_templates.py +20 -0
- gjdutils/pypi_build.py +112 -0
- gjdutils/pytest_utils.py +24 -0
- gjdutils/rand.py +65 -0
- gjdutils/regex.py +78 -0
- gjdutils/requirements_dev.txt +2 -0
- gjdutils/runtime.py +19 -0
- gjdutils/sets.py +5 -0
- gjdutils/shell.py +69 -0
- gjdutils/sorteddict.py +34 -0
- gjdutils/stopwatch.py +79 -0
- gjdutils/strings.py +218 -0
- gjdutils/todo/convert_parquet.py +28 -0
- gjdutils/typ.py +37 -0
- gjdutils/voice_speechrecognition.py +29 -0
- gjdutils/web.py +68 -0
- gjdutils-0.2.2.dist-info/METADATA +101 -0
- gjdutils-0.2.2.dist-info/RECORD +49 -0
- gjdutils-0.2.2.dist-info/WHEEL +4 -0
- gjdutils-0.2.2.dist-info/licenses/LICENSE +21 -0
gjdutils/num.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
# this doesn't support numpy's numeric types, but it's a good stopgap for now.
|
|
4
|
+
# there doesn't appear to be a perfect, agreed solution
|
|
5
|
+
#
|
|
6
|
+
# https://stackoverflow.com/questions/60616802/how-to-type-hint-a-generic-numeric-type-in-python
|
|
7
|
+
Numeric = Union[int, float]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def percent(num, denom):
|
|
11
|
+
return (100 * (num / float(denom))) if denom else 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def percent_str(num, denom):
|
|
15
|
+
return str(percent) + "%"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def discretise(
|
|
19
|
+
val,
|
|
20
|
+
increment: Union[int, float] = 0.1,
|
|
21
|
+
lower: Union[int, float] = 0.0,
|
|
22
|
+
upper: Union[int, float] = 1.0,
|
|
23
|
+
enforce_range: bool = False,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
You will probably want to cache this.
|
|
27
|
+
"""
|
|
28
|
+
import numpy as np
|
|
29
|
+
import pandas as pd
|
|
30
|
+
|
|
31
|
+
def calc_increments(increment, lower, upper):
|
|
32
|
+
assert (
|
|
33
|
+
lower <= increment <= upper
|
|
34
|
+
), f"Required: {lower:.2f} < {increment:.2f} <= {upper:.2f}"
|
|
35
|
+
# e.g. for lower=0, upper=1, increment_size=0.05, nincrements=21
|
|
36
|
+
nincrements = int((upper - lower) / increment) + 1
|
|
37
|
+
# e.g. for lower=0, upper=1, increment_size=0.05, increments = [0., 0.05, 0.1, ..., 0.95, 1. ]
|
|
38
|
+
increments = np.linspace(lower, upper, nincrements)
|
|
39
|
+
return increments
|
|
40
|
+
|
|
41
|
+
if pd.isnull(val):
|
|
42
|
+
return upper
|
|
43
|
+
if enforce_range:
|
|
44
|
+
assert (
|
|
45
|
+
lower <= val <= upper
|
|
46
|
+
), f"Required: {lower:.2f} < {val:.2f} <= {upper:.2f}"
|
|
47
|
+
increments = calc_increments(increment, lower, upper)
|
|
48
|
+
if val < lower:
|
|
49
|
+
return increments[0]
|
|
50
|
+
if val > upper:
|
|
51
|
+
return increments[-1]
|
|
52
|
+
idx = np.digitize(val, increments)
|
|
53
|
+
# e.g.
|
|
54
|
+
# 0.00 -> 0.0
|
|
55
|
+
# 0.01 -> 0.0
|
|
56
|
+
# 0.06 -> 0.05
|
|
57
|
+
# 0.99 -> 0.95
|
|
58
|
+
# 1.00 -> 1.0
|
|
59
|
+
discretised = increments[idx - 1]
|
|
60
|
+
return discretised
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ordinal(n: int):
|
|
64
|
+
"""
|
|
65
|
+
e.g 1 -> "1st", 103 -> "103rd"
|
|
66
|
+
"""
|
|
67
|
+
# from https://claude.ai/chat/87fad336-e0fa-4074-aed4-f4e57ed20bb7
|
|
68
|
+
|
|
69
|
+
# TESTED:
|
|
70
|
+
# for i in [0, 1, 2, 3, 4, 10, 11, 12, 13, 22, 78, 103, 103231, 103235]:
|
|
71
|
+
# print(i, ordinal(i))
|
|
72
|
+
assert n >= 0
|
|
73
|
+
if 10 <= n % 100 <= 20:
|
|
74
|
+
suffix = "th"
|
|
75
|
+
else:
|
|
76
|
+
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
|
|
77
|
+
return f"{n}{suffix}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synthesizes speech from the input string of text or ssml.
|
|
3
|
+
Make sure to be working in a virtual environment.
|
|
4
|
+
https://cloud.google.com/text-to-speech/docs/libraries
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from google.cloud import texttospeech
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def outloud(text: str, language_code: str = "en-GB", bot_gender=None):
|
|
11
|
+
bot_gender = bot_gender.lower() if bot_gender else None
|
|
12
|
+
# not all genders supported for all languages. see https://cloud.google.com/text-to-speech/docs/voices
|
|
13
|
+
if bot_gender is None or bot_gender == "neutral":
|
|
14
|
+
bot_gender = texttospeech.SsmlVoiceGender.NEUTRAL
|
|
15
|
+
elif bot_gender in ["female", texttospeech.SsmlVoiceGender.FEMALE]:
|
|
16
|
+
bot_gender = texttospeech.SsmlVoiceGender.FEMALE
|
|
17
|
+
elif bot_gender in ["male", texttospeech.SsmlVoiceGender.MALE]:
|
|
18
|
+
bot_gender = texttospeech.SsmlVoiceGender.MALE
|
|
19
|
+
else:
|
|
20
|
+
# gender = texttospeech.SsmlVoiceGender.SSML_VOICE_GENDER_UNSPECIFIED
|
|
21
|
+
raise Exception(f"Unknown gender: {bot_gender}")
|
|
22
|
+
# Instantiates a client
|
|
23
|
+
client = texttospeech.TextToSpeechClient()
|
|
24
|
+
|
|
25
|
+
# Set the text input to be synthesized
|
|
26
|
+
synthesis_input = texttospeech.SynthesisInput(text=text)
|
|
27
|
+
# synthesis_input = texttospeech.SynthesisInput(text="Bonjour, Monsieur Natterbot!")
|
|
28
|
+
# synthesis_input = texttospeech.SynthesisInput(text="Γεια σου, Natterbot!")
|
|
29
|
+
|
|
30
|
+
# Build the voice request, select the language code ("en-US") and the ssml
|
|
31
|
+
# voice gender ("neutral")
|
|
32
|
+
voice = texttospeech.VoiceSelectionParams(
|
|
33
|
+
language_code=language_code, ssml_gender=bot_gender
|
|
34
|
+
) # e.g. 'en-GB'
|
|
35
|
+
|
|
36
|
+
# Select the type of audio file you want returned
|
|
37
|
+
audio_config = texttospeech.AudioConfig(
|
|
38
|
+
audio_encoding=texttospeech.AudioEncoding.MP3
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Perform the text-to-speech request on the text input with the selected
|
|
42
|
+
# voice parameters and audio file type
|
|
43
|
+
response = client.synthesize_speech(
|
|
44
|
+
input=synthesis_input, voice=voice, audio_config=audio_config
|
|
45
|
+
)
|
|
46
|
+
return response
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import llm
|
|
2
|
+
import json
|
|
3
|
+
import openai
|
|
4
|
+
import os
|
|
5
|
+
from pprint import pprint
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from gjdutils.llm_utils import proc_llm_out_json
|
|
9
|
+
from gjdutils.llms_openai import (
|
|
10
|
+
DEFAULT_MODEL_NAME,
|
|
11
|
+
MODEL_NAME_GPT4_TURBO,
|
|
12
|
+
OPENAI_API_KEY,
|
|
13
|
+
GranularityTyps,
|
|
14
|
+
call_openai_gpt,
|
|
15
|
+
call_openai_gpt_with_retry,
|
|
16
|
+
call_openai_gpt_with_retry_and_backoff,
|
|
17
|
+
)
|
|
18
|
+
from gjdutils.prompt_templates import summarise_list_of_texts_as_one, summarise_text
|
|
19
|
+
from gjdutils.rand import DEFAULT_RANDOM_SEED
|
|
20
|
+
from gjdutils.strings import jinja_render
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_MODEL = llm.get_model(DEFAULT_MODEL_NAME)
|
|
24
|
+
DEFAULT_MODEL.key = os.environ.get("OPENAI_API_KEY")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def model_from_model_name(model_name: Optional[str] = None, verbose: int = 0):
|
|
28
|
+
if model_name is None:
|
|
29
|
+
model = DEFAULT_MODEL
|
|
30
|
+
else:
|
|
31
|
+
if verbose > 0:
|
|
32
|
+
print("MODEL:", model_name)
|
|
33
|
+
model = llm.get_model(model_name)
|
|
34
|
+
model.key = OPENAI_API_KEY
|
|
35
|
+
return model, model_name
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def llm_prompt(
|
|
39
|
+
prompt: str,
|
|
40
|
+
model_name: Optional[str] = None,
|
|
41
|
+
temperature: Optional[float] = 0.01,
|
|
42
|
+
max_tokens: Optional[int] = None,
|
|
43
|
+
to_json: bool = False,
|
|
44
|
+
verbose: int = 1,
|
|
45
|
+
):
|
|
46
|
+
model, _ = model_from_model_name(model_name)
|
|
47
|
+
response = model.prompt(prompt, temperature=temperature, max_tokens=max_tokens)
|
|
48
|
+
if verbose > 0:
|
|
49
|
+
for chunk in response:
|
|
50
|
+
print(chunk, end="")
|
|
51
|
+
print()
|
|
52
|
+
llm_out = response.text()
|
|
53
|
+
llm_json = proc_llm_out_json(llm_out) if to_json else None
|
|
54
|
+
extra = {
|
|
55
|
+
"prompt": prompt,
|
|
56
|
+
"model_name": model_name,
|
|
57
|
+
"temperature": temperature,
|
|
58
|
+
"max_tokens": max_tokens,
|
|
59
|
+
"response": response,
|
|
60
|
+
"llm_out": llm_out,
|
|
61
|
+
"llm_json": llm_json,
|
|
62
|
+
}
|
|
63
|
+
return llm_json if to_json else llm_out, extra
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def llm_prompt_json(
|
|
67
|
+
prompt: str,
|
|
68
|
+
functions: list[dict],
|
|
69
|
+
model_name: str = MODEL_NAME_GPT4_TURBO,
|
|
70
|
+
temperature: Optional[float] = 0.01,
|
|
71
|
+
verbose: int = 1,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Based on https://platform.openai.com/docs/guides/gpt/function-calling
|
|
75
|
+
|
|
76
|
+
Simon Willison's LLM library doesn't seem to support function calling,
|
|
77
|
+
so we're using the OpenAI Python API directly.
|
|
78
|
+
|
|
79
|
+
Doesn't actually call the function - we're just using the function-calling
|
|
80
|
+
API to ensure we get back json that matches our defined schema.
|
|
81
|
+
|
|
82
|
+
e.g. functions = [
|
|
83
|
+
{
|
|
84
|
+
"name": "get_current_weather",
|
|
85
|
+
"description": "Get the current weather in a given location",
|
|
86
|
+
"parameters": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"location": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "The city and state, e.g. San Francisco, CA",
|
|
92
|
+
},
|
|
93
|
+
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
|
94
|
+
},
|
|
95
|
+
"required": ["location"],
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
"""
|
|
100
|
+
# model, model_name = model_from_model_name(model_name)
|
|
101
|
+
assert functions, "PROMPT_JSON requires at least one function"
|
|
102
|
+
messages = [{"role": "user", "content": prompt}]
|
|
103
|
+
|
|
104
|
+
response = openai.ChatCompletion.create( # type: ignore
|
|
105
|
+
model=model_name,
|
|
106
|
+
messages=messages,
|
|
107
|
+
# if you try and set this to None or [] you get an error, so we'll require at least one actual function (see assert above)
|
|
108
|
+
functions=functions,
|
|
109
|
+
temperature=temperature,
|
|
110
|
+
seed=DEFAULT_RANDOM_SEED,
|
|
111
|
+
)
|
|
112
|
+
response_message = response["choices"][0]["message"] # type: ignore
|
|
113
|
+
|
|
114
|
+
if response_message.get("function_call"):
|
|
115
|
+
function_name = response_message["function_call"]["name"]
|
|
116
|
+
function_args = json.loads(response_message["function_call"]["arguments"])
|
|
117
|
+
llm_out = {
|
|
118
|
+
"role": "function",
|
|
119
|
+
"name": function_name,
|
|
120
|
+
"args": function_args,
|
|
121
|
+
# "content": function_response,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# if call_function:
|
|
125
|
+
# Step 3: call the function
|
|
126
|
+
# # Note: the JSON response may not always be valid; be sure to handle errors
|
|
127
|
+
# available_functions = {
|
|
128
|
+
# "get_current_weather": get_current_weather,
|
|
129
|
+
# } # only one function in this example, but you can have multiple
|
|
130
|
+
# function_to_call = available_functions[function_name]
|
|
131
|
+
# function_response = function_to_call(
|
|
132
|
+
# location=function_args.get("location"),
|
|
133
|
+
# unit=function_args.get("unit"),
|
|
134
|
+
# )
|
|
135
|
+
|
|
136
|
+
# Step 4: send the info on the function call and function response to GPT
|
|
137
|
+
messages.append(response_message) # extend conversation with assistant's reply
|
|
138
|
+
messages.append(
|
|
139
|
+
{
|
|
140
|
+
"role": "function",
|
|
141
|
+
"name": function_name,
|
|
142
|
+
# "content": function_response,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
# if call_function:
|
|
146
|
+
# ) # extend conversation with function response
|
|
147
|
+
# second_response = openai.ChatCompletion.create(
|
|
148
|
+
# model="gpt-3.5-turbo-0613",
|
|
149
|
+
# messages=messages,
|
|
150
|
+
# seed=DEFAULT_RANDOM_SEED,
|
|
151
|
+
# ) # get a new response from GPT where it can see the function response
|
|
152
|
+
# return second_response
|
|
153
|
+
else:
|
|
154
|
+
function_name = None
|
|
155
|
+
function_args = None
|
|
156
|
+
llm_out = response_message["content"]
|
|
157
|
+
|
|
158
|
+
extra = {
|
|
159
|
+
"prompt": prompt,
|
|
160
|
+
"model_name": model_name,
|
|
161
|
+
"functions": functions,
|
|
162
|
+
"temperature": temperature,
|
|
163
|
+
"messages": messages,
|
|
164
|
+
"response": response,
|
|
165
|
+
"response_message": response_message,
|
|
166
|
+
"function_name": function_name,
|
|
167
|
+
"function_args": function_args,
|
|
168
|
+
}
|
|
169
|
+
if verbose > 0:
|
|
170
|
+
if isinstance(llm_out, str):
|
|
171
|
+
print(llm_out)
|
|
172
|
+
else:
|
|
173
|
+
pprint(llm_out)
|
|
174
|
+
return llm_out, extra
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def llm_generate_summary(
|
|
178
|
+
txt_or_txts: str | list[str],
|
|
179
|
+
granularity: Optional[GranularityTyps] = None,
|
|
180
|
+
n_truncate_words=None,
|
|
181
|
+
model_name: Optional[str] = None,
|
|
182
|
+
max_tokens: Optional[int] = None,
|
|
183
|
+
verbose: int = 1,
|
|
184
|
+
):
|
|
185
|
+
"""
|
|
186
|
+
TXT_OR_TXTS can either be a single string,
|
|
187
|
+
or a list of strings (in which case it tries to find the summary that unifies them).
|
|
188
|
+
|
|
189
|
+
TODO: I combined summarisation of text and list into one, but I'm not convinced
|
|
190
|
+
it was such a good idea. It has made things unwieldy. I'm mostly focused on the
|
|
191
|
+
summarisation of a single text for now.
|
|
192
|
+
|
|
193
|
+
TODO maybe we don't need both MAX_TOKENS and N_TRUNCATE_WORDS. Maybe we can just
|
|
194
|
+
use MAX_TOKENS and calculate N_TRUNCATE_WORDS from that.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def do_summarise_text(txt: str):
|
|
198
|
+
if n_truncate_words:
|
|
199
|
+
txt = txt[:n_truncate_words]
|
|
200
|
+
context["txt"] = txt
|
|
201
|
+
prompt = jinja_render(summarise_text, context)
|
|
202
|
+
extra.update(
|
|
203
|
+
{
|
|
204
|
+
"txt": txt, # type: ignore
|
|
205
|
+
}
|
|
206
|
+
) # type: ignore
|
|
207
|
+
return prompt
|
|
208
|
+
|
|
209
|
+
def do_summarise_list(txts: list[str]):
|
|
210
|
+
# UNTESTED
|
|
211
|
+
txts = [txt.replace("\n", " ").replace(" ", " ").strip() for txt in txts]
|
|
212
|
+
if max_tokens is not None:
|
|
213
|
+
if n_truncate_words is None: # type: ignore
|
|
214
|
+
# assume a word is <1.5 tokens. so 3500 / 10 / 1.5 = 233
|
|
215
|
+
n_truncate_words = int(max_tokens / len(txts) / 1.5)
|
|
216
|
+
if n_truncate_words is not None: # type: ignore
|
|
217
|
+
txts = [txt[:n_truncate_words] for txt in txts if txt] # type: ignore
|
|
218
|
+
context["txts"] = txts # type: ignore
|
|
219
|
+
prompt = jinja_render(summarise_list_of_texts_as_one, context)
|
|
220
|
+
extra.update(
|
|
221
|
+
{
|
|
222
|
+
"txts": txts,
|
|
223
|
+
"max_tokens": max_tokens,
|
|
224
|
+
"n_truncate_words": n_truncate_words, # type: ignore
|
|
225
|
+
}
|
|
226
|
+
) # type: ignore
|
|
227
|
+
return prompt
|
|
228
|
+
|
|
229
|
+
if model_name is None:
|
|
230
|
+
model_name = "gpt-3.5-turbo"
|
|
231
|
+
extra = {"input": locals()}
|
|
232
|
+
context = {
|
|
233
|
+
"granularity": (
|
|
234
|
+
"Adjust the length of your summary appropriately, based on the length and complexity of the text. For example, if the text is a paragraph, write a sentence or two. If it's a page, write a paragraph or so. If it's a book, write a page."
|
|
235
|
+
if granularity is None
|
|
236
|
+
else f"Write at most a {granularity}."
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
assert txt_or_txts, "txt_or_txts must be non-empty"
|
|
240
|
+
if isinstance(txt_or_txts, str):
|
|
241
|
+
prompt = do_summarise_text(txt=txt_or_txts)
|
|
242
|
+
elif isinstance(txt_or_txts, list):
|
|
243
|
+
prompt = do_summarise_list(txts=txt_or_txts)
|
|
244
|
+
else:
|
|
245
|
+
raise TypeError("txt_or_txts must be str or list[str]: %s" % type(txt_or_txts))
|
|
246
|
+
|
|
247
|
+
llm_out, llm_extra = llm_prompt(
|
|
248
|
+
prompt, model_name=model_name, max_tokens=max_tokens, verbose=0
|
|
249
|
+
)
|
|
250
|
+
extra.update(
|
|
251
|
+
{
|
|
252
|
+
"context": context,
|
|
253
|
+
"prompt": prompt,
|
|
254
|
+
"llm_out": llm_out,
|
|
255
|
+
"llm_extra": llm_extra,
|
|
256
|
+
} # type: ignore
|
|
257
|
+
)
|
|
258
|
+
if verbose > 0:
|
|
259
|
+
print("Summary:", llm_out)
|
|
260
|
+
if verbose > 1:
|
|
261
|
+
print(f"PROMPT:\n{prompt}")
|
|
262
|
+
extra = {
|
|
263
|
+
"model_name": model_name,
|
|
264
|
+
"prompt": prompt,
|
|
265
|
+
}
|
|
266
|
+
return llm_out, extra
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def llm_fix_json(broken_j: str, model_name: str = "gpt-3.5-turbo"):
|
|
270
|
+
broken_j = broken_j.strip()
|
|
271
|
+
prompt = (
|
|
272
|
+
"""
|
|
273
|
+
Here is some output from an LLM that is supposed to be pure, valid JSON, but it is broken. Return valid JSON, stripping away any extraneous commentary or prose, but maintaining the structure and meaning of the original data as closely as possible.
|
|
274
|
+
|
|
275
|
+
If the json is already valid, return it unchanged.
|
|
276
|
+
|
|
277
|
+
Err on the side of caution: if you're confused about what to do with the input, or if it's ambiguous how best to fix the json, or it is not possible to fix the json, or any other error or uncertainty, return 'ERROR'.
|
|
278
|
+
|
|
279
|
+
----
|
|
280
|
+
|
|
281
|
+
%s
|
|
282
|
+
"""
|
|
283
|
+
% broken_j
|
|
284
|
+
)
|
|
285
|
+
prompt = prompt.strip()
|
|
286
|
+
fixed_j, extra = llm_prompt(prompt, model_name=model_name, temperature=0.001)
|
|
287
|
+
fixed_j = fixed_j.strip() # type: ignore
|
|
288
|
+
try:
|
|
289
|
+
json.loads(fixed_j)
|
|
290
|
+
# TODO check that the before and after are similar lengths and substantially similar strings
|
|
291
|
+
return fixed_j
|
|
292
|
+
except:
|
|
293
|
+
raise
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
# txt = prompt('What is the capital of France?')
|
|
298
|
+
txt = llm_prompt("What is the capital of France?")
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def outloud(
|
|
6
|
+
text: str,
|
|
7
|
+
mp3_filen: str,
|
|
8
|
+
prog: str,
|
|
9
|
+
language_code: Optional[str] = None,
|
|
10
|
+
bot_gender: Optional[str] = None,
|
|
11
|
+
bot_name: Optional[str] = None,
|
|
12
|
+
speed: Optional[str] = None, # or 'slow'
|
|
13
|
+
play: bool = False,
|
|
14
|
+
verbose: int = 0,
|
|
15
|
+
):
|
|
16
|
+
if prog == "google":
|
|
17
|
+
response = outloud_google(
|
|
18
|
+
text=text,
|
|
19
|
+
mp3_filen=mp3_filen, # type: ignore
|
|
20
|
+
language_code=language_code, # type: ignore
|
|
21
|
+
bot_gender=bot_gender,
|
|
22
|
+
speed=speed,
|
|
23
|
+
verbose=verbose,
|
|
24
|
+
)
|
|
25
|
+
elif prog == "azure":
|
|
26
|
+
response = outloud_azure(
|
|
27
|
+
text=text,
|
|
28
|
+
mp3_filen=mp3_filen,
|
|
29
|
+
language_code=language_code,
|
|
30
|
+
bot_gender=bot_gender,
|
|
31
|
+
bot_name=bot_name,
|
|
32
|
+
speed=speed,
|
|
33
|
+
verbose=verbose,
|
|
34
|
+
)
|
|
35
|
+
elif prog == "elevenlabs":
|
|
36
|
+
response = outloud_elevenlabs(
|
|
37
|
+
text=text,
|
|
38
|
+
mp3_filen=mp3_filen,
|
|
39
|
+
bot_name=bot_name,
|
|
40
|
+
speed=speed,
|
|
41
|
+
verbose=verbose,
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
raise Exception(f"Unknown PROG '{prog}'")
|
|
45
|
+
if play:
|
|
46
|
+
from .audios import play_mp3
|
|
47
|
+
|
|
48
|
+
play_mp3(mp3_filen, prog="cli")
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def outloud_azure(
|
|
53
|
+
text: str,
|
|
54
|
+
mp3_filen: str,
|
|
55
|
+
language_code: Optional[str] = "en-GB",
|
|
56
|
+
bot_gender: Optional[str] = None,
|
|
57
|
+
bot_name: Optional[str] = None,
|
|
58
|
+
speed: Optional[str] = None, # or 'slow'
|
|
59
|
+
verbose: int = 0,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
from https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/get-started-text-to-speech?pivots=programming-language-python&tabs=macos%2Cterminal
|
|
63
|
+
"""
|
|
64
|
+
import azure.cognitiveservices.speech as speechsdk
|
|
65
|
+
|
|
66
|
+
# This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
|
|
67
|
+
speech_config = speechsdk.SpeechConfig(
|
|
68
|
+
subscription=os.environ.get("SPEECH_KEY"),
|
|
69
|
+
region=os.environ.get("SPEECH_REGION"),
|
|
70
|
+
)
|
|
71
|
+
# https://learn.microsoft.com/en-us/answers/questions/693848/can-azure-text-to-speech-support-more-audio-files.html
|
|
72
|
+
speech_config.set_speech_synthesis_output_format(
|
|
73
|
+
speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3
|
|
74
|
+
)
|
|
75
|
+
audio_config = speechsdk.audio.AudioOutputConfig(
|
|
76
|
+
use_default_speaker=True, filename=mp3_filen
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# The language of the voice that speaks.
|
|
80
|
+
# speech_config.speech_synthesis_voice_name = "en-US-JennyNeural"
|
|
81
|
+
speech_config.speech_synthesis_voice_name = bot_name
|
|
82
|
+
|
|
83
|
+
speech_synthesizer = speechsdk.SpeechSynthesizer(
|
|
84
|
+
speech_config=speech_config, audio_config=audio_config
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# ssml = f'<speak><prosody rate="30%">{text}</prosody></speak>'
|
|
88
|
+
ssml = f"""
|
|
89
|
+
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
|
|
90
|
+
<voice name="{bot_name}">
|
|
91
|
+
<prosody rate="slow">
|
|
92
|
+
{text}
|
|
93
|
+
</prosody>
|
|
94
|
+
</voice>
|
|
95
|
+
</speak>
|
|
96
|
+
""".strip()
|
|
97
|
+
# print(ssml)
|
|
98
|
+
|
|
99
|
+
# speech_synthesis_result = speech_synthesizer.speak_text_async(text).get()
|
|
100
|
+
speech_synthesis_result = speech_synthesizer.speak_ssml_async(ssml).get()
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
speech_synthesis_result.reason
|
|
104
|
+
== speechsdk.ResultReason.SynthesizingAudioCompleted
|
|
105
|
+
):
|
|
106
|
+
if verbose > 0:
|
|
107
|
+
print("Speech synthesized for text [{}]".format(text))
|
|
108
|
+
elif speech_synthesis_result.reason == speechsdk.ResultReason.Canceled:
|
|
109
|
+
cancellation_details = speech_synthesis_result.cancellation_details
|
|
110
|
+
print("Speech synthesis canceled: {}".format(cancellation_details.reason))
|
|
111
|
+
if cancellation_details.reason == speechsdk.CancellationReason.Error:
|
|
112
|
+
if cancellation_details.error_details:
|
|
113
|
+
print("Error details: {}".format(cancellation_details.error_details))
|
|
114
|
+
print("Did you set the speech resource key and region values?")
|
|
115
|
+
|
|
116
|
+
return speech_synthesis_result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def outloud_google(
|
|
120
|
+
text: str,
|
|
121
|
+
mp3_filen: str,
|
|
122
|
+
language_code: str = "en-GB",
|
|
123
|
+
bot_gender=None,
|
|
124
|
+
speed=None, # or 'slow'
|
|
125
|
+
verbose: int = 0,
|
|
126
|
+
):
|
|
127
|
+
from google.cloud import texttospeech
|
|
128
|
+
|
|
129
|
+
bot_gender = bot_gender.lower() if bot_gender else None
|
|
130
|
+
# not all genders supported for all languages. see https://cloud.google.com/text-to-speech/docs/voices
|
|
131
|
+
if bot_gender is None or bot_gender == "neutral":
|
|
132
|
+
bot_gender = texttospeech.SsmlVoiceGender.NEUTRAL
|
|
133
|
+
elif bot_gender in ["female", texttospeech.SsmlVoiceGender.FEMALE]:
|
|
134
|
+
bot_gender = texttospeech.SsmlVoiceGender.FEMALE
|
|
135
|
+
elif bot_gender in ["male", texttospeech.SsmlVoiceGender.MALE]:
|
|
136
|
+
bot_gender = texttospeech.SsmlVoiceGender.MALE
|
|
137
|
+
else:
|
|
138
|
+
# gender = texttospeech.SsmlVoiceGender.SSML_VOICE_GENDER_UNSPECIFIED
|
|
139
|
+
raise Exception("Unknown gender: %s" % bot_gender)
|
|
140
|
+
# Instantiates a client
|
|
141
|
+
client = texttospeech.TextToSpeechClient()
|
|
142
|
+
|
|
143
|
+
# Set the text input to be synthesized
|
|
144
|
+
if speed is None:
|
|
145
|
+
synthesis_input = texttospeech.SynthesisInput(text=text)
|
|
146
|
+
elif speed == "slow":
|
|
147
|
+
# https://stackoverflow.com/questions/68742170/google-clouds-rate-and-pitch-prosody-attributes
|
|
148
|
+
# ssml = f'<speak><prosody rate="slow">{text}</prosody></speak>'
|
|
149
|
+
ssml = f'<speak><prosody rate="100%">{text}</prosody></speak>'
|
|
150
|
+
synthesis_input = texttospeech.SynthesisInput(ssml=ssml)
|
|
151
|
+
else:
|
|
152
|
+
raise Exception(f"Unknown SPEED '{speed}'")
|
|
153
|
+
# synthesis_input = texttospeech.SynthesisInput(text="Bonjour, Monsieur Natterbot!")
|
|
154
|
+
# synthesis_input = texttospeech.SynthesisInput(text="Γεια σου, Natterbot!")
|
|
155
|
+
|
|
156
|
+
# Build the voice request, select the language code ("en-US") and the ssml
|
|
157
|
+
# voice gender ("neutral")
|
|
158
|
+
voice = texttospeech.VoiceSelectionParams(
|
|
159
|
+
language_code=language_code, # e.g. 'en-GB'
|
|
160
|
+
ssml_gender=bot_gender,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Select the type of audio file you want returned
|
|
164
|
+
audio_config = texttospeech.AudioConfig(
|
|
165
|
+
audio_encoding=texttospeech.AudioEncoding.MP3
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Perform the text-to-speech request on the text input with the selected
|
|
169
|
+
# voice parameters and audio file type
|
|
170
|
+
response = client.synthesize_speech(
|
|
171
|
+
input=synthesis_input, voice=voice, audio_config=audio_config
|
|
172
|
+
)
|
|
173
|
+
with open(mp3_filen, "wb") as out:
|
|
174
|
+
# Write the response to the output file.
|
|
175
|
+
out.write(response.audio_content) # type: ignore
|
|
176
|
+
if verbose > 0:
|
|
177
|
+
print(f"Audio content written to {mp3_filen}")
|
|
178
|
+
|
|
179
|
+
return response
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def outloud_elevenlabs(
|
|
183
|
+
text: str,
|
|
184
|
+
api_key: Optional[str] = None,
|
|
185
|
+
mp3_filen: Optional[str] = None,
|
|
186
|
+
bot_name: Optional[str] = None,
|
|
187
|
+
model: str = "eleven_multilingual_v2",
|
|
188
|
+
# speed=None,
|
|
189
|
+
should_play: bool = False,
|
|
190
|
+
verbose: int = 0,
|
|
191
|
+
):
|
|
192
|
+
from elevenlabs import play, save
|
|
193
|
+
from elevenlabs.client import ElevenLabs
|
|
194
|
+
|
|
195
|
+
if api_key is None:
|
|
196
|
+
api_key = os.environ.get("ELEVENLABS_API_KEY")
|
|
197
|
+
# i should figure out a better way to handle playing an mp3 file
|
|
198
|
+
# assert (
|
|
199
|
+
# not mp3_filen and should_play
|
|
200
|
+
# ), "Writing out uses up the bytes, so you can't then play"
|
|
201
|
+
assert model in ["eleven_multilingual_v2", "eleven_turbo_v2_5"]
|
|
202
|
+
# if speed is not None:
|
|
203
|
+
# raise Exception(f"Unknown SPEED '{speed}'")
|
|
204
|
+
if bot_name is None:
|
|
205
|
+
bot_name = "Charlotte" # slower
|
|
206
|
+
|
|
207
|
+
client = ElevenLabs(
|
|
208
|
+
api_key=api_key,
|
|
209
|
+
)
|
|
210
|
+
audio = client.generate(
|
|
211
|
+
text=text,
|
|
212
|
+
voice=bot_name,
|
|
213
|
+
model=model,
|
|
214
|
+
)
|
|
215
|
+
if mp3_filen is not None:
|
|
216
|
+
save(audio, mp3_filen) # type: ignore
|
|
217
|
+
if should_play:
|
|
218
|
+
if mp3_filen is None:
|
|
219
|
+
play(audio)
|
|
220
|
+
else:
|
|
221
|
+
# if you've already saved, it will consume the bytes
|
|
222
|
+
from .audios import play_mp3
|
|
223
|
+
|
|
224
|
+
play_mp3(mp3_filen, prog="cli")
|
|
225
|
+
return audio
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# mp3_filen = TEMP_MP3_FILEN
|
|
229
|
+
# # response = outloud(text="Hello Natterbot!", mp3_filen=mp3_filen, language_code='en-GB')
|
|
230
|
+
# response = outloud(text="γεια σου", mp3_filen=mp3_filen, language_code='el')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# each of these is a Jinja template. see gjdutils.strings.jinja_render()
|
|
2
|
+
|
|
3
|
+
summarise_text = """
|
|
4
|
+
Summarise the following. Be as concise, concrete, and easy to understand as you can. Provide only the summary itself, without any superfluous conversation, commentary, markup, etc. {{ granularity }}
|
|
5
|
+
|
|
6
|
+
----
|
|
7
|
+
{{ txt }}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# UNTESTED
|
|
12
|
+
summarise_list_of_texts_as_one = """
|
|
13
|
+
Summarise the whole of the following list. Be as concise, concrete, and easy to understand as you can. Provide only the summary itself, without any superfluous conversation or commentary etc. {{ granularity }}
|
|
14
|
+
|
|
15
|
+
----
|
|
16
|
+
{% for txt in txts %}
|
|
17
|
+
- {{txt}}
|
|
18
|
+
{% endfor %}
|
|
19
|
+
----
|
|
20
|
+
"""
|