Add soft French voice and contact saving
This commit is contained in:
@@ -3,3 +3,4 @@ GMAIL_EMAIL=opensquaredgeneva@gmail.com
|
||||
GMAIL_PASSWORD=your_gmail_app_password
|
||||
APP_SECRET=change_me_to_a_long_random_string
|
||||
TTS_PROVIDER=browser
|
||||
CONTACTS_FILE=/root/openbot/data/contacts.json
|
||||
|
||||
@@ -25,7 +25,10 @@ Derniers changements locaux a deployer:
|
||||
- Raccourci optionnel `/usr/local/bin/openbot`.
|
||||
- Mode mains libres: `Demarrer` ecoute en continu, envoi apres 2 secondes de silence.
|
||||
- TTS par defaut via voix navigateur `fr-FR` pour eviter l'accent anglais Groq.
|
||||
- Cache PWA: opensquared-assistant-v5.
|
||||
- Voix navigateur adoucie avec preference pour voix feminine francaise.
|
||||
- Ajout de contacts email par conversation.
|
||||
- Carnet de contacts prod dans `/root/openbot/data/contacts.json`.
|
||||
- Cache PWA: opensquared-assistant-v6.
|
||||
```
|
||||
|
||||
## VPS
|
||||
@@ -176,6 +179,9 @@ Correctif applique:
|
||||
- Le bouton `Demarrer` lance le mode mains libres.
|
||||
- Le message vocal est envoye automatiquement apres 2 secondes de silence.
|
||||
- La voix par defaut est celle du navigateur en `fr-FR`, via `TTS_PROVIDER=browser`.
|
||||
- La version v6 privilegie une voix feminine francaise si disponible sur l'appareil.
|
||||
- Les contacts peuvent etre ajoutes en disant par exemple: `Ajoute Sophie avec l'adresse sophie@example.com`.
|
||||
- Le service systemd definit `CONTACTS_FILE=/root/openbot/data/contacts.json`.
|
||||
|
||||
Si le bouton ne reagit pas apres un deploiement:
|
||||
|
||||
|
||||
23
README.md
23
README.md
@@ -7,8 +7,9 @@ POC de PWA mobile pour parler a l'assistant depuis un telephone.
|
||||
- Interface mobile installable sur Android via "Ajouter a l'ecran d'accueil".
|
||||
- Bouton micro avec transcription Groq Whisper.
|
||||
- Reponse LLM Groq.
|
||||
- Lecture audio automatique de la reponse apres interaction utilisateur.
|
||||
- Lecture audio automatique de la reponse avec une voix francaise douce cote navigateur.
|
||||
- Envoi d'emails Gmail avec carnet de contacts `contacts.json`.
|
||||
- Ajout de contacts par conversation, par exemple : `Ajoute Sophie avec l'adresse sophie@example.com`.
|
||||
|
||||
## Configuration locale
|
||||
|
||||
@@ -25,6 +26,7 @@ GMAIL_EMAIL=...
|
||||
GMAIL_PASSWORD=...
|
||||
APP_SECRET=...
|
||||
TTS_PROVIDER=browser
|
||||
CONTACTS_FILE=/root/openbot/data/contacts.json
|
||||
```
|
||||
|
||||
Installer :
|
||||
@@ -121,10 +123,29 @@ sudo certbot --nginx -d assistant.ton-domaine.com
|
||||
6. Parler naturellement : apres 2 secondes de silence, le message est envoye.
|
||||
7. Appuyer sur `Arreter` pour couper l'ecoute continue.
|
||||
|
||||
## Contacts email
|
||||
|
||||
Les contacts sont stockes dans `contacts.json` en local. Sur le VPS, le service utilise `/root/openbot/data/contacts.json` pour eviter de modifier les fichiers suivis par Git.
|
||||
|
||||
Tu peux les ajouter directement dans l'assistant :
|
||||
|
||||
```text
|
||||
Ajoute Sophie avec l'adresse sophie@example.com
|
||||
```
|
||||
|
||||
Ensuite tu peux dire :
|
||||
|
||||
```text
|
||||
Envoie un email a Sophie
|
||||
```
|
||||
|
||||
L'assistant remplacera `Sophie` par son adresse email.
|
||||
|
||||
## Notes
|
||||
|
||||
- Groq Orpheus TTS doit etre accepte dans la console Groq pour fonctionner :
|
||||
https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english
|
||||
- Par defaut `TTS_PROVIDER=browser` utilise la voix francaise du telephone pour eviter l'accent anglais du modele Groq Orpheus.
|
||||
- La PWA privilegie les voix feminines francaises disponibles sur le telephone, avec un debit plus lent et un ton plus doux.
|
||||
- Pour revenir au TTS Groq, mettre `TTS_PROVIDER=groq` dans `.env`.
|
||||
- La lecture audio automatique depend toujours des regles du navigateur mobile, mais elle marche mieux dans une PWA apres un tap utilisateur que dans Telegram.
|
||||
|
||||
41
app.py
41
app.py
@@ -15,7 +15,7 @@ load_dotenv()
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
STATIC_DIR = ROOT / "static"
|
||||
CONTACTS_FILE = ROOT / "contacts.json"
|
||||
CONTACTS_FILE = Path(os.getenv("CONTACTS_FILE", ROOT / "contacts.json"))
|
||||
|
||||
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
||||
GMAIL_EMAIL = os.getenv("GMAIL_EMAIL", "")
|
||||
@@ -42,6 +42,10 @@ destinataire, sujet, contenu.
|
||||
Quand tu as toutes les informations pour envoyer un email, reponds uniquement avec ce JSON :
|
||||
{"action": "envoyer_email", "destinataire": "...", "sujet": "...", "corps": "..."}
|
||||
|
||||
Quand l'utilisateur veut ajouter, memoriser ou modifier un contact email, collecte le nom et l'adresse email.
|
||||
Quand tu as les deux informations, reponds uniquement avec ce JSON :
|
||||
{"action": "ajouter_contact", "nom": "...", "email": "..."}
|
||||
|
||||
Pour tout le reste, reponds en francais naturel, avec des phrases courtes.
|
||||
N'utilise pas d'anglais sauf si l'utilisateur le demande explicitement.
|
||||
En mode voiture, sois bref : une ou deux phrases maximum."""
|
||||
@@ -54,6 +58,14 @@ def charger_contacts():
|
||||
return {"laurent": "barontil@hotmail.com"}
|
||||
|
||||
|
||||
def sauvegarder_contacts(contacts):
|
||||
CONTACTS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CONTACTS_FILE.write_text(
|
||||
json.dumps(contacts, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def construire_system_prompt():
|
||||
contacts = charger_contacts()
|
||||
lignes = [f"- {nom.title()} : {email}" for nom, email in contacts.items()]
|
||||
@@ -66,7 +78,7 @@ def resoudre_contact(destinataire):
|
||||
return contacts.get(valeur.lower(), valeur)
|
||||
|
||||
|
||||
def extraire_action_email(reponse):
|
||||
def extraire_action(reponse):
|
||||
decodeur = json.JSONDecoder()
|
||||
for index, caractere in enumerate(reponse):
|
||||
if caractere != "{":
|
||||
@@ -75,11 +87,24 @@ def extraire_action_email(reponse):
|
||||
data, _ = decodeur.raw_decode(reponse[index:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(data, dict) and data.get("action") == "envoyer_email":
|
||||
if isinstance(data, dict) and data.get("action") in {"envoyer_email", "ajouter_contact"}:
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def ajouter_contact(nom, email):
|
||||
contacts = charger_contacts()
|
||||
nom_normalise = " ".join(nom.strip().lower().split())
|
||||
email_normalise = email.strip()
|
||||
|
||||
if not nom_normalise or "@" not in email_normalise:
|
||||
raise ValueError("Contact incomplet")
|
||||
|
||||
contacts[nom_normalise] = email_normalise
|
||||
sauvegarder_contacts(contacts)
|
||||
return nom_normalise, email_normalise
|
||||
|
||||
|
||||
def envoyer_email(destinataire, sujet, corps):
|
||||
destinataire = resoudre_contact(destinataire)
|
||||
msg = MIMEMultipart()
|
||||
@@ -163,13 +188,18 @@ def generer_audio(texte):
|
||||
|
||||
def traiter_message(session_id, texte, avec_audio=False):
|
||||
reponse = demander_au_llm(session_id, texte)
|
||||
action = extraire_action_email(reponse)
|
||||
action = extraire_action(reponse)
|
||||
email_sent = False
|
||||
contact_saved = False
|
||||
|
||||
if action:
|
||||
if action and action.get("action") == "envoyer_email":
|
||||
destinataire = envoyer_email(action["destinataire"], action["sujet"], action["corps"])
|
||||
reponse = f"Email envoye a {destinataire}."
|
||||
email_sent = True
|
||||
elif action and action.get("action") == "ajouter_contact":
|
||||
nom, email = ajouter_contact(action["nom"], action["email"])
|
||||
reponse = f"Contact ajoute : {nom.title()}."
|
||||
contact_saved = True
|
||||
|
||||
audio_url = None
|
||||
if avec_audio and TTS_PROVIDER == "groq":
|
||||
@@ -179,6 +209,7 @@ def traiter_message(session_id, texte, avec_audio=False):
|
||||
"reply": reponse,
|
||||
"audioUrl": audio_url,
|
||||
"emailSent": email_sent,
|
||||
"contactSaved": contact_saved,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ After=network.target
|
||||
[Service]
|
||||
WorkingDirectory=/root/openbot
|
||||
EnvironmentFile=/root/openbot/.env
|
||||
Environment=CONTACTS_FILE=/root/openbot/data/contacts.json
|
||||
ExecStart=/root/openbot/.venv/bin/gunicorn -w 2 -b 0.0.0.0:8080 app:app
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
@@ -323,7 +323,8 @@ async function sendVoiceBlob(blob) {
|
||||
async function handleAssistantResponse(data) {
|
||||
const reply = data.reply || "";
|
||||
addBubble(reply, "assistant");
|
||||
setStatus(data.emailSent ? "Email envoye" : "Pret", data.emailSent ? "live" : "idle");
|
||||
const status = data.emailSent ? "Email envoye" : data.contactSaved ? "Contact ajoute" : "Pret";
|
||||
setStatus(status, data.emailSent || data.contactSaved ? "live" : "idle");
|
||||
|
||||
if (!audioToggle.checked || !reply) return;
|
||||
|
||||
@@ -350,26 +351,45 @@ function speakInFrench(text) {
|
||||
window.speechSynthesis.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = "fr-FR";
|
||||
utterance.rate = 0.98;
|
||||
utterance.pitch = 1;
|
||||
utterance.voice = pickFrenchVoice();
|
||||
utterance.rate = 0.9;
|
||||
utterance.pitch = 1.08;
|
||||
utterance.volume = 0.92;
|
||||
utterance.voice = pickSoftFrenchVoice();
|
||||
utterance.onend = resolve;
|
||||
utterance.onerror = resolve;
|
||||
window.speechSynthesis.speak(utterance);
|
||||
});
|
||||
}
|
||||
|
||||
function pickFrenchVoice() {
|
||||
function pickSoftFrenchVoice() {
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
const frenchVoices = voices.filter((voice) => voice.lang && voice.lang.toLowerCase().startsWith("fr"));
|
||||
const preferredNames = [
|
||||
"audrey",
|
||||
"aurelie",
|
||||
"aurélie",
|
||||
"marie",
|
||||
"julie",
|
||||
"celine",
|
||||
"céline",
|
||||
"lea",
|
||||
"léa",
|
||||
"amelie",
|
||||
"amélie",
|
||||
"sylvie",
|
||||
"hortense"
|
||||
];
|
||||
|
||||
return (
|
||||
voices.find((voice) => voice.lang === "fr-FR" && /google|microsoft/i.test(voice.name)) ||
|
||||
voices.find((voice) => voice.lang === "fr-FR") ||
|
||||
voices.find((voice) => voice.lang && voice.lang.toLowerCase().startsWith("fr")) ||
|
||||
frenchVoices.find((voice) => preferredNames.some((name) => voice.name.toLowerCase().includes(name))) ||
|
||||
frenchVoices.find((voice) => voice.lang === "fr-FR" && /female|femme|google|microsoft/i.test(voice.name)) ||
|
||||
frenchVoices.find((voice) => voice.lang === "fr-FR") ||
|
||||
frenchVoices[0] ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
window.speechSynthesis?.addEventListener?.("voiceschanged", pickFrenchVoice);
|
||||
window.speechSynthesis?.addEventListener?.("voiceschanged", pickSoftFrenchVoice);
|
||||
|
||||
async function resetConversation() {
|
||||
await fetch("/api/reset", {
|
||||
|
||||
@@ -50,6 +50,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/app.js?v=5" type="module"></script>
|
||||
<script src="/app.js?v=6" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const CACHE_NAME = "opensquared-assistant-v5";
|
||||
const CACHE_NAME = "opensquared-assistant-v6";
|
||||
const ASSETS = [
|
||||
"/",
|
||||
"/styles.css",
|
||||
"/app.js?v=5",
|
||||
"/app.js?v=6",
|
||||
"/manifest.webmanifest",
|
||||
"/icons/icon-v2.svg"
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user