Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

user can talk (en/de/zh/es) and ai can speak, script modularized #4

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import threading # TEST


from flask import Flask, render_template, request
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit

from partygpt import AiGuest
Expand All @@ -14,6 +14,7 @@
PATH_SETTINGS = 'settings.yml'
APPLICATION_SETTINGS = read_yaml(path=PATH_SETTINGS)['application']
PATH_FOLDER_CONVERSATION_RECORDS = APPLICATION_SETTINGS['conversation_records_folder']
PATH_FOLDER_CONVERSATION_RECORDS_NAME = APPLICATION_SETTINGS['records_folder_name']
REFRESH_TIMER_AI_GOODBYE = APPLICATION_SETTINGS['refresh_conversation_after_ai_says_goodbye']

app = Flask(__name__)
Expand All @@ -27,7 +28,7 @@
log_console_handler = logging.StreamHandler()
log_console_handler.setLevel(logging.INFO)
log_console_handler.setFormatter(log_formatter)
log_file_handler = logging.FileHandler('debug.log')
log_file_handler = logging.FileHandler('debug.log', encoding='utf-8')
log_file_handler.setLevel(logging.DEBUG)
log_file_handler.setFormatter(log_formatter)
root_logger = logging.getLogger()
Expand All @@ -46,6 +47,7 @@ def log_connect(data):

@app.route('/')
def index():
ai_guest.set_conversation_record_folder_path(PATH_FOLDER_CONVERSATION_RECORDS_NAME)
return render_template('index.html')

@app.route('/process-input', methods=['POST'])
Expand Down Expand Up @@ -74,6 +76,26 @@ def refresh_session():
ai_guest.reset(persona=None)
return '' # may not return None or omit return statement

@app.route('/save-records', methods=['GET'])
def save_records():
ai_guest.save_messages_history()
return '', 200

@app.route('/set-records', methods=['GET'])
def set_records():
flag = request.args.get('flag')
logger.info(f'Chat recording is set to: {flag}')
response_data = {}
if flag == 'true': # (?)flag param can only be interpreted as string
ai_guest.set_conversation_record_folder_path(PATH_FOLDER_CONVERSATION_RECORDS_NAME)
response_data['message'] = 'Chat will be recorded'
else:
ai_guest.set_conversation_record_folder_path('')
response_data['message'] = 'Chat will not be recorded'

response = jsonify(response_data)
return response, 200, {'Content-Type': 'application/json'}

def trigger_session_refresh(goodbye_msg):
logger.info('Session refreshed (per backend).')
sockets.emit('instruction', {
Expand Down
16 changes: 13 additions & 3 deletions partygpt/partygpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ def get_accumulated_costs(self, verbose=True):
return total_t['sum']

def _clear_messages_history(self) -> None:
self._messages_history = []

def save_messages_history(self) -> None:
if self._conversation_record_folder_path and self._messages_history: # only record if path defined
logger.info('Chat records is now saving...')
try:
os.makedirs(self._conversation_record_folder_path, exist_ok=True)
file_name = os.path.join(self._conversation_record_folder_path,
str(self._current_conversation_start) + '.convrec')
with open(file_name, 'x') as f:
with open(file_name, 'x', encoding='utf-8') as f:
for el in self._messages_history:
role_ = el['role']
content_ = el['content']
Expand All @@ -63,8 +67,7 @@ def _clear_messages_history(self) -> None:
logger.info(f'Wrote conversation to {file_name}.')
except Exception as e:
logger.error(f'Trouble writing conversation history to file: {type(e).__name__}: {e}')

self._messages_history = []


def set_functions(
self,
Expand All @@ -87,6 +90,13 @@ def reset(self):
self._clear_messages_history()
self._current_conversation_start = int(time.time())

def set_conversation_record_folder_path(self, new_path):
self._conversation_record_folder_path = new_path
if new_path != '':
logger.info(f'Chat will be recorded to {new_path}')
else:
logger.info('Chat will not be recorded')

def communicate(
self,
messages: list,
Expand Down
7 changes: 5 additions & 2 deletions settings.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
application:
refresh_conversation_after_ai_says_goodbye: 30 # seconds
conversation_records_folder: conversation_records # remove the characters after ":" to deactivate conversation recording

records_folder_name: &name_anchor conversation_records
conversation_records_folder: *name_anchor # remove the characters after ":" to deactivate conversation recording

model:
id: gpt-3.5-turbo

Expand Down Expand Up @@ -36,6 +37,8 @@ guest:
special_skill: # you can ___.
- tell funny jokes
- provide facts about science and nature in an understandable way
- talk like Barney Stinson
- talk like Winnie the Pooh
- talk like a pirate
- create incredible drinks (you actually named the party's signature drink "Rum Royale Bliss")
- start playing a quiz on basic knowledge with the guest
Expand Down
93 changes: 71 additions & 22 deletions static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,62 @@ h1 {
margin-bottom: 10px;
}

.mode-container {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
text-align: center;
}

.button-container {
display: flex;
justify-content: space-between;
}

.button-container button {
background-color: #4caf50;
color: #fff;
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
}

#send-button:hover {
background-color: #45a049;
}

#close-button {
background-color: #f44336;
}

#close-button:hover {
background-color: #d32f2f;
}

#mode-toggle-button {
background-color: #2fd2f3;
}


#chat-info {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}

#recording-info {
margin-right: 10px;
}

#user-input {
flex: 1;
padding: 10px;
font-size: 16px;
border: none;
width: 100%;
border-radius: 5px;
}

Expand Down Expand Up @@ -72,30 +123,28 @@ h1 {
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
white-space: pre-wrap;
} /* TODO: class for common properties of user-input and ai-message */

#send-button,
#close-button {
background-color: #4caf50;
color: #fff;
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
}

#send-button:hover {
background-color: #45a049;
}
/* TODO: class for common properties of user-input and ai-message */

#close-button {
position: absolute;
bottom: 20px;
right: 20px;
background-color: #f44336;
}

#close-button:hover {
background-color: #d32f2f;

.icon-container {
display: inline-block;
text-decoration: none;
padding: 10px;
border-radius: 50%;
/* Makes it a circle */
background-color: #007bff;
color: #fff;
/* Text color */
transition: background-color 0.3s ease;
/* Smooth background color transition */
margin-left: 30px;
}

#audio-input-icon {
width: 24px;
height: 24px;
vertical-align: middle;
}
Binary file added static/images/mic-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
127 changes: 127 additions & 0 deletions static/js/audio.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { handleUserInput } from './script.mjs';
import { franc } from 'https://esm.sh/franc@6?bundle'; //https://github.com/wooorm/franc
import { iso6393To1 } from './iso6393-to-bcp47.mjs';

//https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API#speech_recognition
var recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
var isSpeakMode = false;
var isNowSpeaking = false;


export function textToSpeech(message, isByClicking) {
//Only in speak mode the new message will be spoken automatically
//every time when a new reply is received, this function will be called from script.mjs.
//But script.mjs doesn't hold the information whether it is speak mode now, so this condition will be checked here firstly

if (isNowSpeaking && isByClicking) {
console.log('User stopped the speaking');
speechSynthesis.cancel();
return;
}

if ((isSpeakMode || isByClicking)) {
console.log('message to speak:', message);
//https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance
var utterance = new SpeechSynthesisUtterance();

utterance.onstart = function (event) {
console.log('Speech started');
isNowSpeaking = true;
};

utterance.onend = function (event) {
console.log('Speech ended');
isNowSpeaking = false;
};

utterance.onerror = function (event) {
//In tested chrome browser the speaking sometimes abrupts without calling onerror or onend
//but with Edge it is ok.
console.error('Speech synthesis error:', event.error);
isNowSpeaking = false;
};

var detectedLanguage = franc(message);
console.log("Detected language code by franc:", detectedLanguage);
utterance.lang = mapISO6393toISO6391(detectedLanguage) || 'en-US';
//for code "zh", chrome uses mandarin, and edge uses cantonese :O maybe use BCP47 language code will be exacter
console.log("Language code in ISO639-1:", utterance.lang);
utterance.text = message;
utterance.rate = 1.3;
speechSynthesis.speak(utterance);
}
}

export function setUpSpeechRecognition() {
recognition.continuous = true; // Enable continuous recognition

recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
};

recognition.onstart = () => {
console.log('Speech recognition started.');
};

recognition.addEventListener('result', (event) => {
const transcript = event.results[0][0].transcript;
console.log('Recognized text:', transcript);
recognition.stop();
let message = { value: transcript };
handleUserInput(message);
});
};


function toggleMode(btn) {
console.log("Mode toggled. IsSpeakMode:", isSpeakMode);

var speakModeDiv = document.getElementById('speak-mode-container');
var writeModeDiv = document.getElementById('write-mode-container');

if (isSpeakMode) {
btn.textContent = 'Write Mode';
speakModeDiv.style.display = 'block';
writeModeDiv.style.display = 'none';

var LanguageDropdown = document.getElementById("language-dropdown");
LanguageDropdown.addEventListener('change', () => {
//console.log('change language');
setSpeechRecognitionLanguage(LanguageDropdown.value);
});

var audioInputIcon = document.getElementById("audio-input-icon");
audioInputIcon.addEventListener('click', () => {
recognition.start();
});

setUpSpeechRecognition();
setSpeechRecognitionLanguage(LanguageDropdown.value);
} else {
btn.textContent = 'Speak Mode';
speakModeDiv.style.display = 'none';
writeModeDiv.style.display = 'block';
var userInput = document.getElementById("user-input");
userInput.focus();
}
}

function setSpeechRecognitionLanguage(lang) {
console.log('set language to: ', lang);
recognition.lang = lang;
};


function mapISO6393toISO6391(iso6393Code) {
return iso6393To1[iso6393Code];
}

document.addEventListener("DOMContentLoaded", function () {

var modeToggleButton = document.getElementById("mode-toggle-button");

modeToggleButton.addEventListener("click", function () {
isSpeakMode = !isSpeakMode;
toggleMode(modeToggleButton);
});
});
Loading