diff --git a/Dockerfile b/Dockerfile
index fe05a1b..78228d8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,9 @@
# Use the official Python base image
FROM python:3.11-slim
+# Install ffmpeg
+RUN apt-get update && apt-get install -y ffmpeg
+
# Set the working directory in the container
WORKDIR /app
diff --git a/app.py b/app.py
index b7f36a6..53279ad 100644
--- a/app.py
+++ b/app.py
@@ -41,10 +41,9 @@ def serve_sitemap():
gtag('config', 'G-THNE3MSS49');
-
-
+
-
+
diff --git a/assets/app.css b/assets/app.css
index 61da9e6..c0db1c6 100644
--- a/assets/app.css
+++ b/assets/app.css
@@ -31,7 +31,7 @@ body {
margin: auto;
max-width: 600px;
min-height: 470px;
- padding: 0px 20px;
+ padding: 0px 20px 10px;
}
#conversation {
display: block;
@@ -68,6 +68,13 @@ body {
margin: 10px 0px -15px;
width: fit-content;
}
+#intro {
+ color: #333;
+ font-size: 20px;
+ font-style: italic;
+ margin: 0px 20px 20px;
+ text-align: center;
+}
.languages {
display: flex;
min-width: 100%;
@@ -102,6 +109,9 @@ body {
float: left;
max-width: 75%;
padding: 5px 10px;
+ text-decoration: underline;
+ text-decoration-color: #87b7ff;
+ text-decoration-style: dotted;
width: fit-content;
}
.message-user {
@@ -109,6 +119,9 @@ body {
border-radius: 4px 0px 4px 4px;
clear: both;
padding: 5px 10px;
+ text-decoration: underline;
+ text-decoration-color: #000;
+ text-decoration-style: dotted;
width: fit-content;
}
.message-user-wrapper {
@@ -117,15 +130,28 @@ body {
margin: 15px 0px;
width: 100%;
}
-#toggle-play-audio-text {
+#audio-settings-text {
color: #aaa;
font-size: 14px;
font-style: italic;
margin-right: 10px;
padding-top: 2px;
}
-#toggle-play-audio-wrapper {
+#audio-settings {
+ display: flex;
+}
+#toggle-play-audio-div {
display: flex;
+ margin: 0px 120px 10px 0px;
+}
+#slider-audio-speed-div {
+ display: flex;
+ margin: 0px 0px 10px;
+}
+#audio-speed {
+ margin-left: 10px;
+ margin-top: 5px;
+ width: 50px;
}
#translation {
clear: both;
@@ -154,3 +180,15 @@ body {
#user-response-text::placeholder {
color: #aaa;
}
+
+@media screen and (max-width: 600px) {
+ #audio-settings {
+ display: block;
+ }
+ #intro {
+ font-size: 18px;
+ }
+ #toggle-play-audio-div {
+ margin: 0px;
+ }
+}
diff --git a/assets/audio.py b/assets/audio.py
index 0563a12..e01a09e 100644
--- a/assets/audio.py
+++ b/assets/audio.py
@@ -1,9 +1,10 @@
import base64
from gtts import gTTS
+from pydub import AudioSegment
-def get_audio_file(text: str, language: str) -> str:
+def get_audio_file(text: str, language: str, playback_speed: float) -> str:
"""
Create and return an mp3 file that contains the audio
for a message to be played in the desired language's accent.
@@ -21,10 +22,18 @@ def get_audio_file(text: str, language: str) -> str:
audio_path = "temp_audio.mp3"
tts.save(audio_path)
- # Read and encode the audio file
- with open(audio_path, "rb") as audio_file:
+ # Create a new audio segment with adjusted speed
+ audio = AudioSegment.from_file(audio_path)
+ playback_speed = 1 + (playback_speed / 100)
+ adjusted_audio = audio.speedup(playback_speed=playback_speed)
+
+ # Save the adjusted audio to a new file
+ adjusted_audio_file = f"adjusted_audio.mp3"
+ adjusted_audio.export(adjusted_audio_file, format="mp3")
+
+ with open(adjusted_audio_file, "rb") as audio_file:
audio_data = audio_file.read()
audio_base64 = base64.b64encode(audio_data).decode("utf-8")
audio_src = f"data:audio/mpeg;base64,{audio_base64}"
- return audio_src
+ return audio_src
diff --git a/assets/chat_request.py b/assets/chat_request.py
index 0a22d22..12f034e 100755
--- a/assets/chat_request.py
+++ b/assets/chat_request.py
@@ -3,12 +3,13 @@
import time
from typing import Dict, List
-import openai
+
import requests
from dash import Input, Output, callback, no_update
+from openai import OpenAI
from tenacity import retry, stop_after_attempt, wait_random_exponential
-openai.api_key = os.environ.get("OPENAI_KEY")
+client = OpenAI(api_key=os.environ.get("OPENAI_KEY"))
@callback(
@@ -36,10 +37,11 @@ def convert_audio_recording_to_text(check_for_audio_file: bool) -> str:
while check_for_audio_file:
if os.path.exists(audio_recording):
-
audio_file = open(audio_recording, "rb")
os.remove(audio_recording)
- transcript = openai.Audio.transcribe("whisper-1", audio_file)
+ transcript = client.audio.transcriptions.create(
+ model="whisper-1", file=audio_file
+ )
message_user = transcript.to_dict()["text"]
return message_user, {"display": "none"}, False
@@ -62,7 +64,7 @@ def get_assistant_message(messages: List[Dict[str, str]]) -> str:
"""
chat_response = _chat_completion_request(messages)
- message_assistant = chat_response.json()["choices"][0]["message"]["content"]
+ message_assistant = chat_response.choices[0].message.content
# Remove space before "!" or "?"
message_assistant = re.sub(r"\s+([!?])", r"\1", message_assistant)
@@ -82,22 +84,11 @@ def _chat_completion_request(messages: List[Dict[str, str]]) -> Dict:
A response from OpenAI's model to the user's statement.
"""
- headers = {
- "Content-Type": "application/json",
- "Authorization": "Bearer " + openai.api_key,
- }
- json_data = {
- "model": "gpt-3.5-turbo-0613",
- "messages": messages,
- "temperature": 1.5, # Higher values provide more varied responses
- }
try:
- response = requests.post(
- "https://api.openai.com/v1/chat/completions",
- headers=headers,
- json=json_data,
+ completion = client.chat.completions.create(
+ model="gpt-4o-mini", temperature=1.5, max_tokens=50, messages=messages
)
- return response
+ return completion
except Exception as e:
return e
@@ -120,7 +111,8 @@ def system_content(
The content message for the system.
"""
- content = f"Start a conversation about {conversation_setting} in {language_learn}. \
+ content = f"Act as an excellent {language_learn} teacher who is helping me to practice {language_learn}. \
+ Start a conversation about {conversation_setting} in {language_learn}. \
Provide one statement in {language_learn}, then wait for my response. \
Do not write in {language_known}. \
Always finish your response with a question. \
diff --git a/assets/footer.css b/assets/footer.css
index 0b78b80..8cf85b8 100644
--- a/assets/footer.css
+++ b/assets/footer.css
@@ -1,15 +1,16 @@
#buy-me-a-coffee-logo {
max-width: 100px;
+ margin-top: -3px;
}
#email {
- font-size: 14px;
text-align: center;
}
#footer {
border-top: 1px solid #cccccc;
display: flex;
+ font-size: 13px;
justify-content: center;
- margin: 20px auto 0px;
+ margin: 20px auto 5px;
width: 100%;
}
#footer a {
@@ -23,6 +24,9 @@
margin-top: 25px;
}
@media (max-width: 800px) {
+ #buy-me-a-coffee-logo {
+ margin-top: 0px;
+ }
#footer {
display: block;
padding: 20px;
diff --git a/assets/message_correction.py b/assets/message_correction.py
new file mode 100644
index 0000000..45f559e
--- /dev/null
+++ b/assets/message_correction.py
@@ -0,0 +1,46 @@
+import os
+
+from openai import OpenAI
+from tenacity import retry, stop_after_attempt, wait_random_exponential
+
+client = OpenAI(api_key=os.environ.get("OPENAI_KEY"))
+
+
+def get_corrected_message(message: str, language_learn: str) -> str:
+ """
+ Get and process the assistant's (OpenAI's model) message to continue the conversation.
+
+ Params:
+ message: The message from the assistant.
+ language_learn: The language that the user wants to learn.
+
+ Returns:
+ The corrected message from the assistant.
+ """
+
+ message_corrected = _chat_completion_request(message, language_learn)
+ if message_corrected != message:
+ return message_corrected
+
+
+@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))
+def _chat_completion_request(message: str, language_learn: str) -> str:
+ """
+ Request a response to the user's statement from one of OpenAI's chat models.
+
+ Params:
+ messages: The conversation history between the user and the chat model.
+ language_learn: The language that the user wants to learn.
+
+ Returns:
+ The corrected message from OpenAI's model.
+ """
+
+ try:
+ content = f"You are an excellent {language_learn} teacher. Correct this sentence for any mistakes:\n{message}"
+ completion = client.chat.completions.create(
+ model="gpt-3.5-turbo", messages=[{"role": "system", "content": content}]
+ )
+ return completion.choices[0].message.content
+ except Exception as e:
+ return e
diff --git a/footer.py b/footer.py
index be76661..688d7ad 100644
--- a/footer.py
+++ b/footer.py
@@ -4,6 +4,8 @@
footer = html.Div(id='footer', children=[
html.P("Practice a Language. All rights reserved."),
html.Div("|", className="footer-pipe"),
+ dcc.Link("About", href="/about"),
+ html.Div("|", className="footer-pipe"),
html.A("We're open source!", target="_blank", href="https://github.com/Currie32/practice-a-language"),
html.Div("|", className="footer-pipe"),
html.A(
@@ -14,7 +16,7 @@
html.Div("|", className="footer-pipe"),
html.P("david.currie32@gmail.com"),
html.Div("|", className="footer-pipe"),
- dcc.Link("Terms & Conditions", href="/terms"),
+ dcc.Link("Terms", href="/terms"),
html.Div("|", className="footer-pipe"),
dcc.Link("Privacy Policy", href="/privacy_policy"),
])
diff --git a/pages/about.py b/pages/about.py
new file mode 100644
index 0000000..5de2fbe
--- /dev/null
+++ b/pages/about.py
@@ -0,0 +1,37 @@
+from dash import html, register_page
+
+
+register_page(__name__, path="/about")
+
+meta_tags = [
+ {
+ "name": "description",
+ "content": "Practice A Language - Learn and practice languages through conversations.",
+ },
+]
+
+layout = html.Div(
+ id="content",
+ children=[
+ html.H1("About Practice a Language"),
+ html.P(
+ "Welcome to Practice A Language, a website to help you practice a language by having conversations. This website started from wanting to make it easier to learn a language before going on trips abroad. I became annoyed with the over-repetition of apps like Duolingo and losing track of how many times I translated “Juan come manzanas”."
+ ),
+ html.H2("Learn what you want faster"),
+ html.P(
+ "Unlike other tools that force you to learn according to their lesson plans, you can practice the conversation topics and phrases that you want, whenever you want. This control should help you to be ready for your next trip abroad much faster."
+ ),
+ html.H2("Practice at your level"),
+ html.P(
+ "You chat in either the language you’re learning or your native language. This allows experienced speakers to practice their vocabulary and grammar, while beginners can write in their native language and it will automatically be translated into the language they are learning."
+ ),
+ html.H2("Practice writing and speaking"),
+ html.P(
+ "You have the choice to practice your new language by either writing your response or recording your voice. If you record your voice, it will be transcribed so that you can see what was understood. If you want to make a change, then you can edit the text or rerecord yourself."
+ ),
+ html.H2("Learn from your mistakes"),
+ html.P(
+ "When speaking or writing in your new language, your responses are always analyzed for mistakes and will be automatically corrected. This quick feedback will help you to learn more from each conversation."
+ ),
+ ],
+)
diff --git a/pages/home.py b/pages/home.py
index 7e08ab9..fbdbf42 100644
--- a/pages/home.py
+++ b/pages/home.py
@@ -2,8 +2,18 @@
import dash_bootstrap_components as dbc
import dash_daq as daq
-from dash import (Input, Output, State, callback, callback_context,
- clientside_callback, dcc, html, no_update, register_page)
+from dash import (
+ Input,
+ Output,
+ State,
+ callback,
+ callback_context,
+ clientside_callback,
+ dcc,
+ html,
+ no_update,
+ register_page,
+)
from dash_selectable import DashSelectable
from deep_translator import GoogleTranslator
from gtts import lang
@@ -11,18 +21,27 @@
from langdetect.lang_detect_exception import LangDetectException
from assets.audio import get_audio_file
-from assets.chat_request import (convert_audio_recording_to_text,
- get_assistant_message, system_content)
+from assets.chat_request import (
+ convert_audio_recording_to_text,
+ get_assistant_message,
+ system_content,
+)
+from assets.message_correction import get_corrected_message
from callbacks.conversation_settings import (
- start_conversation_button_disabled, update_conversation_setting_values)
-from callbacks.display_components import (display_conversation_helpers,
- display_user_input,
- is_user_recording_audio,
- loading_visible)
+ start_conversation_button_disabled,
+ update_conversation_setting_values,
+)
+from callbacks.display_components import (
+ display_conversation_helpers,
+ display_user_input,
+ is_user_recording_audio,
+ loading_visible,
+)
from callbacks.placeholder_text import user_input_placeholder
from callbacks.tooltips import tooltip_translate_language_known_text
from callbacks.translate import translate_highlighted_text
+
register_page(__name__, path="")
MESSAGES = []
LANGUAGES_DICT = {name: abbreviation for abbreviation, name in lang.tts_langs().items()}
@@ -35,6 +54,14 @@
html.Div(
id="content",
children=[
+ html.Div(
+ id="intro",
+ children=[
+ html.P(
+ children="Practice a language by having conversations about the topic of your choice."
+ ),
+ ],
+ ),
# Language selection section
html.Div(
className="languages",
@@ -75,11 +102,19 @@
children=[
dcc.Dropdown(
[
+ "a book you read",
+ "a movie you watched",
+ "a recent holiday",
+ "a restaurant you went to",
"asking for directions",
"booking a hotel",
"buying a bus ticket",
"buying groceries",
"cooking a meal",
+ "favourite foods",
+ "going to a concert",
+ "going to a movie",
+ "going to a restaurant",
"going to a show",
"hobbies",
"making a dinner reservation",
@@ -88,13 +123,13 @@
"ordering at a cafe",
"ordering at a restaurant",
"pets",
- "recent movies",
+ "planning a trip",
"renting a car",
"shopping in a store",
"weekend plans",
"other",
],
- placeholder="Choose a setting",
+ placeholder="Choose a topic",
id="conversation-setting",
),
],
@@ -105,7 +140,7 @@
children=[
dbc.Input(
id="conversation-setting-custom",
- placeholder="Or type a custom setting for a conversation",
+ placeholder="Or type a custom topic for a conversation",
type="text",
),
],
@@ -113,14 +148,35 @@
],
),
# Toggle to play audio of new messages
- html.P(
- id="toggle-play-audio-wrapper",
+ html.Div(
+ id="audio-settings",
children=[
- html.P(
- "Play audio of new message", id="toggle-play-audio-text"
+ html.Div(
+ id="toggle-play-audio-div",
+ children=[
+ html.P(
+ "Play audio of new message",
+ id="audio-settings-text",
+ ),
+ daq.ToggleSwitch(
+ id="toggle-play-audio", value=True, color="#322CA1"
+ ),
+ ],
),
- daq.ToggleSwitch(
- id="toggle-play-audio", value=True, color="#322CA1",
+ html.Div(
+ id="slider-audio-speed-div",
+ children=[
+ html.P("Audio speed", id="audio-settings-text"),
+ daq.Slider(
+ id="audio-speed",
+ min=1,
+ max=21,
+ value=11,
+ step=10,
+ size=100,
+ color="#322CA1",
+ ),
+ ],
),
],
),
@@ -169,29 +225,36 @@
],
),
# Helper icons and tooltip about writing and recording user response
- html.Div(id="user-response-helper-icons", children=[
- html.Div(children=[
- html.I(
- className="bi bi-question-circle",
- id="help-translate-language-known",
- ),
- dbc.Tooltip(
- id="tooltip-translate-language-known",
- target="help-translate-language-known",
- ),
- ]),
- html.Div(children=[
- html.I(
- className="bi bi-question-circle",
- id="help-change-microphone-setting",
+ html.Div(
+ id="user-response-helper-icons",
+ children=[
+ html.Div(
+ children=[
+ html.I(
+ className="bi bi-question-circle",
+ id="help-translate-language-known",
+ ),
+ dbc.Tooltip(
+ id="tooltip-translate-language-known",
+ target="help-translate-language-known",
+ ),
+ ]
),
- dbc.Tooltip(
- id="tooltip-change-microphone-setting",
- target="help-change-microphone-setting",
- children="If you are unable to record audio, you might need to change your device's microphone settings."
+ html.Div(
+ children=[
+ html.I(
+ className="bi bi-question-circle",
+ id="help-change-microphone-setting",
+ ),
+ dbc.Tooltip(
+ id="tooltip-change-microphone-setting",
+ target="help-change-microphone-setting",
+ children="If you are unable to record audio, you might need to change your device's microphone settings.",
+ ),
+ ]
),
- ]),
- ]),
+ ],
+ ),
# User response section
html.Div(
id="user-response",
@@ -210,13 +273,15 @@
id="button-submit-response-text",
n_clicks=0,
),
- ]
+ ],
),
],
style={"display": "none"},
),
# Boolean for when to look for the user's audio recording
dcc.Store(id="check-for-audio-file", data=False),
+ # Store for messages
+ dcc.Store(id="messages-store", data=[]),
],
),
],
@@ -228,6 +293,7 @@
@callback(
Output("conversation", "children", allow_duplicate=True),
Output("loading", "style", allow_duplicate=True),
+ Output("messages-store", "data", allow_duplicate=True),
Input("button-start-conversation", "n_clicks"),
State("language-known", "value"),
State("language-learn", "value"),
@@ -258,17 +324,13 @@ def start_conversation(
The display value for the loading icons.
"""
- # Use the global variables inside the callback
- global MESSAGES
-
# Replace conversation_setting with conversation_setting_custom if it has a value
if conversation_setting_custom:
conversation_setting = conversation_setting_custom
if button_start_conversation_n_clicks:
-
- MESSAGES = []
- MESSAGES.append(
+ messages = []
+ messages.append(
{
"role": "system",
# Provide content about the conversation for the system (OpenAI's GPT)
@@ -281,10 +343,10 @@ def start_conversation(
)
# Get the first message in the conversation from OpenAI's GPT
- message_assistant = get_assistant_message(MESSAGES)
- # message_assistant = 'Guten morgen!' # <- Testing message
+ message_assistant = get_assistant_message(messages)
+ # message_assistant = 'Guten morgen, wie kann ich ihnen helfen!' # <- Testing message
- MESSAGES.append({"role": "assistant", "content": message_assistant})
+ messages.append({"role": "assistant", "content": message_assistant})
# Create a list to store the conversation history
conversation = [
@@ -304,25 +366,27 @@ def start_conversation(
# For initial audio play
html.Audio(id="audio-player-0", autoPlay=True),
# Need two audio elements to always provide playback after conversation has been created
- html.Audio(id=f"audio-player-1-1", autoPlay=True),
- html.Audio(id=f"audio-player-1-2", autoPlay=True),
+ html.Audio(id=f"audio-player-1", autoPlay=True),
+ html.Audio(id=f"audio-player-2", autoPlay=True),
],
)
]
- return conversation, {"display": "none"}
+ return conversation, {"display": "none"}, messages
@callback(
Output("conversation", "children", allow_duplicate=True),
Output("user-response-text", "value", allow_duplicate=True),
Output("loading", "style", allow_duplicate=True),
+ Output("messages-store", "data", allow_duplicate=True),
Input("user-response-text", "n_submit"),
Input("button-submit-response-text", "n_clicks"),
State("user-response-text", "value"),
State("conversation", "children"),
State("language-known", "value"),
State("language-learn", "value"),
+ State("messages-store", "data"),
prevent_initial_call="initial_duplicate",
)
def continue_conversation_text(
@@ -332,6 +396,7 @@ def continue_conversation_text(
conversation: List,
language_known: str,
language_learn: str,
+ messages_store: List[Dict[str, str]],
) -> Tuple[List, str, Dict[str, str]]:
"""
Continue the conversation by adding the user's response, then calling OpenAI
@@ -344,6 +409,7 @@ def continue_conversation_text(
conversation: The conversation between the user and OpenAI's GPT.
language_known: The language that the user speaks.
language_learn: The language that the user wants to learn.
+ messages_store: Store of messages.
Returns:
The conversation with the new messages from the user and OpenAI's GPT.
@@ -351,13 +417,9 @@ def continue_conversation_text(
The new display value to hide the loading icons.
"""
- # Use the global variable inside the callback
- global MESSAGES
-
if (
user_response_n_submits is not None or button_submit_n_clicks is not None
) and message_user:
-
try:
language_detected = detect(message_user)
if language_detected == LANGUAGES_DICT[language_known]:
@@ -366,27 +428,32 @@ def continue_conversation_text(
target=LANGUAGES_DICT[language_learn],
)
message_user = translator.translate(message_user)
+ else:
+ message_user = get_corrected_message(message_user, language_learn)
except LangDetectException:
pass
- MESSAGES.append({"role": "user", "content": message_user})
- message_new = format_new_message("user", len(MESSAGES), message_user)
+ messages = messages_store.copy()
+ messages.append({"role": "user", "content": message_user})
+ message_new = format_new_message("user", len(messages), message_user)
conversation = conversation + message_new
- messages_to_send = [MESSAGES[0]] + MESSAGES[1:][-4:]
+ messages_to_send = [messages[0]] + messages[1:][-5:]
message_assistant = get_assistant_message(messages_to_send)
# message_assistant = 'Natürlich!' # <- testing message
- MESSAGES.append({"role": "assistant", "content": message_assistant})
- message_new = format_new_message("ai", len(MESSAGES), message_assistant)
+ messages.append({"role": "assistant", "content": message_assistant})
+ message_new = format_new_message("ai", len(messages), message_assistant)
conversation = conversation + message_new
- return conversation, "", {"display": "none"}
+ return conversation, "", {"display": "none"}, messages
return no_update
-def format_new_message(who: str, messages_count: int, message: str) -> List[html.Div]:
+def format_new_message(
+ who: str, messages_count: int, message: str, message_corrected: str = ""
+) -> List[html.Div]:
"""
Format a new message so that it is ready to be added to the conversation.
@@ -408,27 +475,33 @@ def format_new_message(who: str, messages_count: int, message: str) -> List[html
id=f"message-{messages_count - 1}",
children=[message],
),
+ html.Div(
+ className=f"message-{who}-corrected",
+ id=f"message-{messages_count - 1}-corrected",
+ children=[message_corrected],
+ ),
html.Div(
html.I(className="bi bi-play-circle", id="button-play-audio"),
id=f"button-message-{messages_count - 1}",
className="button-play-audio-wrapper",
),
# Need two audio elements to always provide playback
- html.Audio(id=f"audio-player-{messages_count - 1}-1", autoPlay=True),
- html.Audio(id=f"audio-player-{messages_count - 1}-2", autoPlay=True),
+ html.Audio(id=f"audio-player-1", autoPlay=True),
+ html.Audio(id=f"audio-player-2", autoPlay=True),
],
)
]
@callback(
- Output("audio-player-0", "src"),
+ Output("audio-player-1", "src"),
Input("conversation", "children"),
State("toggle-play-audio", "value"),
+ State("audio-speed", "value"),
State("language-learn", "value"),
)
def play_newest_message(
- conversation: List, toggle_audio: bool, language_learn: str
+ conversation: List, toggle_audio: bool, audio_speed: int, language_learn: str
) -> str:
"""
Play the newest message in the conversation.
@@ -436,6 +509,7 @@ def play_newest_message(
Params:
conversation: Contains all of the data about the conversation
toggle_audio: Whether to play the audio of the newest message
+ audio_speed: The speed of the audio
language_learn: The language that the user wants to learn.
Returns:
@@ -443,11 +517,12 @@ def play_newest_message(
"""
if conversation and toggle_audio:
-
- newest_message = conversation[-1]["props"]["children"][0]["props"]["children"][0]
+ newest_message = conversation[-1]["props"]["children"][0]["props"]["children"][
+ 0
+ ]
language_learn_abbreviation = LANGUAGES_DICT[language_learn]
- return get_audio_file(newest_message, language_learn_abbreviation)
+ return get_audio_file(newest_message, language_learn_abbreviation, audio_speed)
return no_update
@@ -457,15 +532,20 @@ def play_newest_message(
for i in range(100):
@callback(
- Output(f"audio-player-{i+1}-1", "src"),
- Output(f"audio-player-{i+1}-2", "src"),
+ Output(f"audio-player-1", "src", allow_duplicate=True),
+ Output(f"audio-player-2", "src", allow_duplicate=True),
Input(f"button-message-{i+1}", "n_clicks"),
State(f"conversation", "children"),
+ State("toggle-play-audio", "value"),
+ State("audio-speed", "value"),
State("language-learn", "value"),
+ prevent_initial_call="initial_duplicate",
)
def play_audio_of_clicked_message(
button_message_n_clicks: int,
conversation: List,
+ toggle_audio: bool,
+ audio_speed: int,
language_learn: str,
) -> str:
"""
@@ -474,14 +554,15 @@ def play_audio_of_clicked_message(
Params:
button_message_n_clicks: The number of times the play-audio button was clicked.
conversation: The conversation between the user and OpenAI's GPT.
+ toggle_audio: Whether to play the audio of the new message
+ audio_speed: The speed of the audio
language_learn: The language that the user wants to learn.
Returns:
A path to the message's audio that is to be played
"""
- if button_message_n_clicks:
-
+ if button_message_n_clicks and toggle_audio:
triggered_input_id = callback_context.triggered[0]["prop_id"].split(".")[0]
message_number_clicked = triggered_input_id.split("-")[-1]
@@ -495,12 +576,14 @@ def play_audio_of_clicked_message(
# Rotate between audio elements so that the audio is always played
if button_message_n_clicks % 2 == 0:
return (
- get_audio_file(message_clicked, language_learn_abbreviation),
+ get_audio_file(
+ message_clicked, language_learn_abbreviation, audio_speed
+ ),
"",
)
else:
return "", get_audio_file(
- message_clicked, language_learn_abbreviation
+ message_clicked, language_learn_abbreviation, audio_speed
)
return ("", "")
diff --git a/pages/privacy_policy.py b/pages/privacy_policy.py
index 5711bac..9e84cda 100644
--- a/pages/privacy_policy.py
+++ b/pages/privacy_policy.py
@@ -3,99 +3,170 @@
register_page(__name__, path="/privacy_policy")
-layout = html.Div(id='content', children=[
- html.H3("Practice a Language Privacy Policy"),
- html.P("Type of website: Practice a language by speaking and writing"),
- html.P("Effective date: November 14th, 2023"),
- html.P("www.practicealanguage.xyz (the \"Site\") is owned and operated by David Currie Software Development Ltd.. David Currie Software Development Ltd. is the data controller and can be contacted at: david.currie32@gmail.com"),
- html.H4("Purpose"),
- html.P("The purpose of this privacy policy (this \"Privacy Policy\") is to inform users of our Site of the following:"),
- html.P("1. The personal data we will collect;"),
- html.P("2. Use of collected data;"),
- html.P("3. Who has access to the data collected;"),
- html.P("4. The rights of Site users; and"),
- html.P("5. The Site's cookie policy."),
- html.P("This Privacy Policy applies in addition to the terms and conditions of our Site."),
- html.H4("GDPR"),
- html.P("For users in the European Union, we adhere to the Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016, known as the General Data Protection Regulation (the \"GDPR\"). For users in the United Kingdom, we adhere to the GDPR as enshrined in the Data Protection Act 2018."),
- html.H4("Constent"),
- html.P("By using our Site users agree that they consent to:"),
- html.P("1. The conditions set out in this Privacy Policy."),
- html.P("When the legal basis for us processing your personal data is that you have provided your consent to that processing, you may withdraw your consent at any time. If you withdraw your consent, it will not make processing which we completed before you withdrew your consent unlawful."),
- html.P("You can withdraw your consent by emailing us at david.currie32@gmail.com."),
- html.H4("Legal Basis for Processing"),
- html.P("We collect and process personal data about users in the EU only when we have a legal basis for doing so under Article 6 of the GDPR."),
- html.P("We rely on the following legal basis to collect and process the personal data of users in the EU:"),
- html.P("1. Users have provided their consent to the processing of their data for one or more specific purposes."),
- html.H4("Personal Data We Collect"),
- html.P("We only collect data that helps us achieve the purpose set out in this Privacy Policy. We will not collect any additional data beyond the data listed below without notifying you first."),
- html.H4("Data Collected Automatically"),
- html.P("When you visit and use our Site, we may automatically collect and store the following information:"),
- html.P("1. IP address;"),
- html.P("2. Location"),
- html.P("3. Hardware and software details; and"),
- html.P("4. Clicked links."),
- html.H4("Data Collected in a Non-Automatic Way"),
- html.P("We may also collect the following data when you perform certain functions on our Site:"),
- html.P("1. Choose a conversation setting for practicing."),
- html.P("This data may be collected using the following methods:"),
- html.P("1. With an API call, then stored in our database."),
- html.H4("How We Use Personal Data"),
- html.P("Data collected on our Site will only be used for the purposes specified in this Privacy Policy or indicated on the relevant pages of our Site. We will not use your data beyond what we disclose in this Privacy Policy."),
- html.P("The data we collect automatically is used for the following purposes:"),
- html.P("1. Providing more relevant ads using Google Adsense."),
- html.P("The data we collect when the user performs certain functions may be used for the following purposes:"),
- html.P("1. To add more default conversation settings to the Site."),
- html.H4("Who We Share Personal Data With"),
- html.H5("Employees"),
- html.P("We may disclose user data to any member of our organization who reasonably needs access to user data to achieve the purposes set out in this Privacy Policy."),
- html.H5("Third Parties"),
- html.P("We may share user data with the following third parties:"),
- html.P("1. Google Adsense."),
- html.P("We may share the follwoing user data with third parties:"),
- html.P("1. User IP addresses, browsing histories, website preferences, device location and device preferences."),
- html.P("We may share user data with third parties for the following purposes:"),
- html.P("1. Targeted advertising."),
- html.P("Third parties will not be able to access user data beyond what is reasonably necessary to achieve the given purpose."),
- html.H5("Other Disclosures"),
- html.P("We will not sell or share your data with other third parties, except in the following cases:"),
- html.P("1. If the law requries it;"),
- html.P("2. If it is required for any legal proceeding;"),
- html.P("3. To prove or protect our legal rights; and"),
- html.P("4. To buyers or potential buyers of this company in the event that we seek to sell the company."),
- html.P("If you follow hyperlinks from our Site to another site, please note that we are not responsible for and have no control over their privacy policies and practices."),
- html.H4("How Long We Store Personal Data"),
- html.P("User data will be stored until the purpose the data was collected for has been achieved."),
- html.P("You will be notified if your data is kept for longer than this period."),
- html.H4("How We Protect Your Personal Data"),
- html.P("The company will use products developed and provided by Google to store personal data."),
- html.P("While we take all reasonable precautions to ensure that user data is secure and that users are protected, there always remains the risk of harm. The Internet as a whole can be insecure at times and therefore we are unable to guarantee the security of user data beyond what is reasonably practical."),
- html.H4("Your Rights as a User"),
- html.P("Under the GDPR, you have the following rights:"),
- html.P("1. Right to be informed;"),
- html.P("2. Right of access;"),
- html.P("3. Right to rectification;"),
- html.P("4. Right to erasure;"),
- html.P("5. Right to restrict processing;"),
- html.P("6. Right to data protability; and"),
- html.P("7. Right to object."),
- html.H4("Children"),
- html.P("We do not knowingly collect or use personal data from children under 16 years of age. If we learn that we have collected personal data from a child under 16 years of age, the personal data will be deleted as soon as possible. If a child under 16 years of age has provided us with personal data their parent or guardian may contact the company."),
- html.H4("How to Access, Modify, Delete, or Challenge the Data Collected"),
- html.P("If you would like to know if we have collected your personal data, how we have used your personal data, if we have disclosed your personal data and to who we disclosed your personal data, if you would like your data to be deleted or modified in any way, or if you would like to exercise any of your other rights under the GDPR, please contact us at: david.currie32@gmail.com"),
- html.H4("How to Opt-Out of Data Collection, Use or Disclosure"),
- html.P("In addition to the method(s) described in the How to Access, Modify, Delete, or Challenge the Data Collected section, we provide the following specific opt-out methods for the forms of collection, use, or disclosure of your personal data:"),
- html.P("1. All collected data. You can opt-out by selecting that they do not consent to their data being collected."),
- html.H4("Cookie Policy"),
- html.P("A cookie is a small file, stored on a user's hard drive by a website. Its purpose is to collect data relating to the user's browsing habits. You can choose to be notified each time a cookie is transmitted. You can also choose to disable cookies entirely in your internet browser, but this may decrease the quality of your user experience."),
- html.P("We use the following types of cookies on our Site:"),
- html.H5("1. Third-Party Cookies"),
- html.P("Third-party cookies are created by a website other than ours. We may use third-party cookies to achieve the following purposes:"),
- html.P("1. Monitor user preferences to tailor advertisements around their interests."),
- html.H4("Modifications"),
- html.P("This Privacy Policy may be amended from time to time in order to maintain compliance with the law and to reflect any changes to our data collection process. When we amend this Privacy Policy we will update the \"Effective Date\" at the top of this Privacy Policy. We recommend that our users periodically review our Privacy Policy to ensure that they are notified of any updates. If necessary, we may notify users by email of changes to this Privacy Policy."),
- html.H4("Complaints"),
- html.P("If you have any complaints about how we process your personal data, please contact us through the contact methods listed in the Contact Information section so that we can, where possible, resolve the issue. If you feel we have not addressed your concern in a satisfactory manner you may contact a supervisory authority. You also have the right to directly make a complaint to a supervisory authority. You can lodge a complaint with a supervisory authority by contacting the Information Commissioner's Office in the UK, Data Protection Commission in Ireland."),
- html.H4("Contact Information"),
- html.P("If you have any questions, concerns, or complaints, you can contact us at: david.currie32@gmail.com"),
-])
+layout = html.Div(
+ id="content",
+ children=[
+ html.H3("Practice a Language Privacy Policy"),
+ html.P("Type of website: Practice a language by speaking and writing"),
+ html.P("Effective date: November 14th, 2023"),
+ html.P(
+ 'www.practicealanguage.xyz (the "Site") is owned and operated by David Currie Software Development Ltd.. David Currie Software Development Ltd. is the data controller and can be contacted at: david.currie32@gmail.com'
+ ),
+ html.H4("Purpose"),
+ html.P(
+ 'The purpose of this privacy policy (this "Privacy Policy") is to inform users of our Site of the following:'
+ ),
+ html.P("1. The personal data we will collect;"),
+ html.P("2. Use of collected data;"),
+ html.P("3. Who has access to the data collected;"),
+ html.P("4. The rights of Site users; and"),
+ html.P("5. The Site's cookie policy."),
+ html.P(
+ "This Privacy Policy applies in addition to the terms and conditions of our Site."
+ ),
+ html.H4("GDPR"),
+ html.P(
+ 'For users in the European Union, we adhere to the Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016, known as the General Data Protection Regulation (the "GDPR"). For users in the United Kingdom, we adhere to the GDPR as enshrined in the Data Protection Act 2018.'
+ ),
+ html.H4("Constent"),
+ html.P("By using our Site users agree that they consent to:"),
+ html.P("1. The conditions set out in this Privacy Policy."),
+ html.P(
+ "When the legal basis for us processing your personal data is that you have provided your consent to that processing, you may withdraw your consent at any time. If you withdraw your consent, it will not make processing which we completed before you withdrew your consent unlawful."
+ ),
+ html.P(
+ "You can withdraw your consent by emailing us at david.currie32@gmail.com."
+ ),
+ html.H4("Legal Basis for Processing"),
+ html.P(
+ "We collect and process personal data about users in the EU only when we have a legal basis for doing so under Article 6 of the GDPR."
+ ),
+ html.P(
+ "We rely on the following legal basis to collect and process the personal data of users in the EU:"
+ ),
+ html.P(
+ "1. Users have provided their consent to the processing of their data for one or more specific purposes."
+ ),
+ html.H4("Personal Data We Collect"),
+ html.P(
+ "We only collect data that helps us achieve the purpose set out in this Privacy Policy. We will not collect any additional data beyond the data listed below without notifying you first."
+ ),
+ html.H4("Data Collected Automatically"),
+ html.P(
+ "When you visit and use our Site, we may automatically collect and store the following information:"
+ ),
+ html.P("1. IP address;"),
+ html.P("2. Location"),
+ html.P("3. Hardware and software details; and"),
+ html.P("4. Clicked links."),
+ html.H4("Data Collected in a Non-Automatic Way"),
+ html.P(
+ "We may also collect the following data when you perform certain functions on our Site:"
+ ),
+ html.P("1. Choose a conversation setting for practicing."),
+ html.P("This data may be collected using the following methods:"),
+ html.P("1. With an API call, then stored in our database."),
+ html.H4("How We Use Personal Data"),
+ html.P(
+ "Data collected on our Site will only be used for the purposes specified in this Privacy Policy or indicated on the relevant pages of our Site. We will not use your data beyond what we disclose in this Privacy Policy."
+ ),
+ html.P("The data we collect automatically is used for the following purposes:"),
+ html.P("1. Providing more relevant ads using Google Adsense."),
+ html.P(
+ "The data we collect when the user performs certain functions may be used for the following purposes:"
+ ),
+ html.P("1. To add more default conversation settings to the Site."),
+ html.H4("Who We Share Personal Data With"),
+ html.H5("Employees"),
+ html.P(
+ "We may disclose user data to any member of our organization who reasonably needs access to user data to achieve the purposes set out in this Privacy Policy."
+ ),
+ html.H5("Third Parties"),
+ html.P("We may share user data with the following third parties:"),
+ html.P("1. Google Adsense."),
+ html.P("We may share the follwoing user data with third parties:"),
+ html.P(
+ "1. User IP addresses, browsing histories, website preferences, device location and device preferences."
+ ),
+ html.P("We may share user data with third parties for the following purposes:"),
+ html.P("1. Targeted advertising."),
+ html.P(
+ "Third parties will not be able to access user data beyond what is reasonably necessary to achieve the given purpose."
+ ),
+ html.H5("Other Disclosures"),
+ html.P(
+ "We will not sell or share your data with other third parties, except in the following cases:"
+ ),
+ html.P("1. If the law requries it;"),
+ html.P("2. If it is required for any legal proceeding;"),
+ html.P("3. To prove or protect our legal rights; and"),
+ html.P(
+ "4. To buyers or potential buyers of this company in the event that we seek to sell the company."
+ ),
+ html.P(
+ "If you follow hyperlinks from our Site to another site, please note that we are not responsible for and have no control over their privacy policies and practices."
+ ),
+ html.H4("How Long We Store Personal Data"),
+ html.P(
+ "User data will be stored until the purpose the data was collected for has been achieved."
+ ),
+ html.P(
+ "You will be notified if your data is kept for longer than this period."
+ ),
+ html.H4("How We Protect Your Personal Data"),
+ html.P(
+ "The company will use products developed and provided by Google to store personal data."
+ ),
+ html.P(
+ "While we take all reasonable precautions to ensure that user data is secure and that users are protected, there always remains the risk of harm. The Internet as a whole can be insecure at times and therefore we are unable to guarantee the security of user data beyond what is reasonably practical."
+ ),
+ html.H4("Your Rights as a User"),
+ html.P("Under the GDPR, you have the following rights:"),
+ html.P("1. Right to be informed;"),
+ html.P("2. Right of access;"),
+ html.P("3. Right to rectification;"),
+ html.P("4. Right to erasure;"),
+ html.P("5. Right to restrict processing;"),
+ html.P("6. Right to data protability; and"),
+ html.P("7. Right to object."),
+ html.H4("Children"),
+ html.P(
+ "We do not knowingly collect or use personal data from children under 16 years of age. If we learn that we have collected personal data from a child under 16 years of age, the personal data will be deleted as soon as possible. If a child under 16 years of age has provided us with personal data their parent or guardian may contact the company."
+ ),
+ html.H4("How to Access, Modify, Delete, or Challenge the Data Collected"),
+ html.P(
+ "If you would like to know if we have collected your personal data, how we have used your personal data, if we have disclosed your personal data and to who we disclosed your personal data, if you would like your data to be deleted or modified in any way, or if you would like to exercise any of your other rights under the GDPR, please contact us at: david.currie32@gmail.com"
+ ),
+ html.H4("How to Opt-Out of Data Collection, Use or Disclosure"),
+ html.P(
+ "In addition to the method(s) described in the How to Access, Modify, Delete, or Challenge the Data Collected section, we provide the following specific opt-out methods for the forms of collection, use, or disclosure of your personal data:"
+ ),
+ html.P(
+ "1. All collected data. You can opt-out by selecting that they do not consent to their data being collected."
+ ),
+ html.H4("Cookie Policy"),
+ html.P(
+ "A cookie is a small file, stored on a user's hard drive by a website. Its purpose is to collect data relating to the user's browsing habits. You can choose to be notified each time a cookie is transmitted. You can also choose to disable cookies entirely in your internet browser, but this may decrease the quality of your user experience."
+ ),
+ html.P("We use the following types of cookies on our Site:"),
+ html.H5("1. Third-Party Cookies"),
+ html.P(
+ "Third-party cookies are created by a website other than ours. We may use third-party cookies to achieve the following purposes:"
+ ),
+ html.P(
+ "1. Monitor user preferences to tailor advertisements around their interests."
+ ),
+ html.H4("Modifications"),
+ html.P(
+ 'This Privacy Policy may be amended from time to time in order to maintain compliance with the law and to reflect any changes to our data collection process. When we amend this Privacy Policy we will update the "Effective Date" at the top of this Privacy Policy. We recommend that our users periodically review our Privacy Policy to ensure that they are notified of any updates. If necessary, we may notify users by email of changes to this Privacy Policy.'
+ ),
+ html.H4("Complaints"),
+ html.P(
+ "If you have any complaints about how we process your personal data, please contact us through the contact methods listed in the Contact Information section so that we can, where possible, resolve the issue. If you feel we have not addressed your concern in a satisfactory manner you may contact a supervisory authority. You also have the right to directly make a complaint to a supervisory authority. You can lodge a complaint with a supervisory authority by contacting the Information Commissioner's Office in the UK, Data Protection Commission in Ireland."
+ ),
+ html.H4("Contact Information"),
+ html.P(
+ "If you have any questions, concerns, or complaints, you can contact us at: david.currie32@gmail.com"
+ ),
+ ],
+)
diff --git a/pages/terms.py b/pages/terms.py
index e332af0..6b1e02e 100644
--- a/pages/terms.py
+++ b/pages/terms.py
@@ -3,24 +3,47 @@
register_page(__name__, path="/terms")
-layout = html.Div(id='content', children=[
- html.H3("TERMS AND CONDITIONS"),
- html.P("These terms and conditions (the \"Terms and Conditions\") govern the user of www.practicealanguage.xyz (the \"Site\"). This Site is owned and operated by David Currie Software Development Ltd.. This Site is for helps its users to practice a language by writing and speaking."),
- html.P("By using this Site, you indicate that you have read and understand these Terms and Conditions and agree to abide by them at all times."),
- html.H4("Intellectual Property"),
- html.P("All content published and made available on our Site is the property of David Currie Software Development Ltd. and the Site's creators. This includes, but is not limited to images, text, logos, documents, and anything that contributes to the composition of our Site."),
- html.H4("Links to Other Websites"),
- html.P("Our Site contains links to third party websites or services that we do not own or control. We are not responsible for the content, policies, or practices of any third party website or service linked to on our Site. It is your responsibility to read the terms and conditions and privacy policies of these third party websites before using these sites."),
- html.H4("Limitation of Liability"),
- html.P("David Currie Software Development Ltd. and our directors, employees, and affiliates will not be liable for any actions, claims, losses, damages, liabilities and expenses including legal fees from your use of the Site."),
- html.H4("Indemnity"),
- html.P("Except where prohibited by law, by using this Site you indemnify and hold harmless David Currie Software Development Ltd. and our directors, employees, and affiliates from any actions, claims, losses, damages, liabilities, and expenses including legal fees arising out of your use of our Site or your violation of these Terms and Conditions."),
- html.H4("Applicable Law"),
- html.P("These Terms and Conditions are governed by the laws of the Province of British Columbia."),
- html.H4("Severability"),
- html.P("If at any time any of the provisions set forth in these Terms and Conditions are found to be inconsistent or invalid under applicable laws, those provisions will be deemed void and will be removed from these Terms and Conditions. All other provisions will not be affected by the removal and the rest of these Terms and Conditions will still be considered valid."),
- html.H4("Changes"),
- html.P("These Terms and Conditions may be amended from time to time in order to maintain compliance with the law and to reflect any changes to the way we operate our Site and the way we expect users to behave on our Site."),
- html.H4("Contact Details"),
- html.P("Please contact us if you have any questions or concerns at: david.currie32@gmail.com"),
-])
+layout = html.Div(
+ id="content",
+ children=[
+ html.H3("TERMS AND CONDITIONS"),
+ html.P(
+ 'These terms and conditions (the "Terms and Conditions") govern the user of www.practicealanguage.xyz (the "Site"). This Site is owned and operated by David Currie Software Development Ltd.. This Site is for helps its users to practice a language by writing and speaking.'
+ ),
+ html.P(
+ "By using this Site, you indicate that you have read and understand these Terms and Conditions and agree to abide by them at all times."
+ ),
+ html.H4("Intellectual Property"),
+ html.P(
+ "All content published and made available on our Site is the property of David Currie Software Development Ltd. and the Site's creators. This includes, but is not limited to images, text, logos, documents, and anything that contributes to the composition of our Site."
+ ),
+ html.H4("Links to Other Websites"),
+ html.P(
+ "Our Site contains links to third party websites or services that we do not own or control. We are not responsible for the content, policies, or practices of any third party website or service linked to on our Site. It is your responsibility to read the terms and conditions and privacy policies of these third party websites before using these sites."
+ ),
+ html.H4("Limitation of Liability"),
+ html.P(
+ "David Currie Software Development Ltd. and our directors, employees, and affiliates will not be liable for any actions, claims, losses, damages, liabilities and expenses including legal fees from your use of the Site."
+ ),
+ html.H4("Indemnity"),
+ html.P(
+ "Except where prohibited by law, by using this Site you indemnify and hold harmless David Currie Software Development Ltd. and our directors, employees, and affiliates from any actions, claims, losses, damages, liabilities, and expenses including legal fees arising out of your use of our Site or your violation of these Terms and Conditions."
+ ),
+ html.H4("Applicable Law"),
+ html.P(
+ "These Terms and Conditions are governed by the laws of the Province of British Columbia."
+ ),
+ html.H4("Severability"),
+ html.P(
+ "If at any time any of the provisions set forth in these Terms and Conditions are found to be inconsistent or invalid under applicable laws, those provisions will be deemed void and will be removed from these Terms and Conditions. All other provisions will not be affected by the removal and the rest of these Terms and Conditions will still be considered valid."
+ ),
+ html.H4("Changes"),
+ html.P(
+ "These Terms and Conditions may be amended from time to time in order to maintain compliance with the law and to reflect any changes to the way we operate our Site and the way we expect users to behave on our Site."
+ ),
+ html.H4("Contact Details"),
+ html.P(
+ "Please contact us if you have any questions or concerns at: david.currie32@gmail.com"
+ ),
+ ],
+)
diff --git a/requirements.txt b/requirements.txt
index d971388..4d3bdc3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ deep-translator==1.11.4
Flask==2.2.5
gTTS==2.3.2
langdetect==1.0.9
-openai==0.27.8
+openai==1.12.0
orjson==3.9.5
+pydub==0.25.1
tenacity==8.2.2
diff --git a/sitemap.xml b/sitemap.xml
index 5b38fb0..1094fbb 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -1,11 +1,17 @@
-
+
https://practicealanguage.xyz/
- 2023-10-14
+ 2024-07-29
monthly
1.0
+
+ https://practicealanguage.xyz/about
+ 2024-02-20
+ yearly
+ 0.5
+
https://practicealanguage.xyz/terms
2023-10-14