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.
Files changed (49) hide show
  1. gjdutils/__init__.py +12 -0
  2. gjdutils/audios.py +39 -0
  3. gjdutils/cacheing.py +237 -0
  4. gjdutils/cmd.py +149 -0
  5. gjdutils/colab.py +39 -0
  6. gjdutils/collections.py +36 -0
  7. gjdutils/decorators.py +34 -0
  8. gjdutils/dicts.py +216 -0
  9. gjdutils/dsci.py +202 -0
  10. gjdutils/dt.py +296 -0
  11. gjdutils/env.py +64 -0
  12. gjdutils/errors.py +12 -0
  13. gjdutils/files.py +140 -0
  14. gjdutils/functions.py +6 -0
  15. gjdutils/google_translate.py +80 -0
  16. gjdutils/hashing.py +32 -0
  17. gjdutils/html.py +87 -0
  18. gjdutils/indexing.py +97 -0
  19. gjdutils/iterfunc.py +99 -0
  20. gjdutils/jsons.py +70 -0
  21. gjdutils/lists.py +13 -0
  22. gjdutils/llm_utils.py +167 -0
  23. gjdutils/llms_claude.py +131 -0
  24. gjdutils/llms_openai.py +299 -0
  25. gjdutils/misc.py +30 -0
  26. gjdutils/num.py +77 -0
  27. gjdutils/obsolete/google_text_to_speech.py +46 -0
  28. gjdutils/obsolete/llms_obsolete.py +298 -0
  29. gjdutils/outloud_text_to_speech.py +230 -0
  30. gjdutils/prompt_templates.py +20 -0
  31. gjdutils/pypi_build.py +112 -0
  32. gjdutils/pytest_utils.py +24 -0
  33. gjdutils/rand.py +65 -0
  34. gjdutils/regex.py +78 -0
  35. gjdutils/requirements_dev.txt +2 -0
  36. gjdutils/runtime.py +19 -0
  37. gjdutils/sets.py +5 -0
  38. gjdutils/shell.py +69 -0
  39. gjdutils/sorteddict.py +34 -0
  40. gjdutils/stopwatch.py +79 -0
  41. gjdutils/strings.py +218 -0
  42. gjdutils/todo/convert_parquet.py +28 -0
  43. gjdutils/typ.py +37 -0
  44. gjdutils/voice_speechrecognition.py +29 -0
  45. gjdutils/web.py +68 -0
  46. gjdutils-0.2.2.dist-info/METADATA +101 -0
  47. gjdutils-0.2.2.dist-info/RECORD +49 -0
  48. gjdutils-0.2.2.dist-info/WHEEL +4 -0
  49. 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
+ """