tranquillity 0.1.0__tar.gz
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.
- tranquillity-0.1.0/PKG-INFO +23 -0
- tranquillity-0.1.0/README.md +91 -0
- tranquillity-0.1.0/ctOS.py +43 -0
- tranquillity-0.1.0/docker_launcher.py +37 -0
- tranquillity-0.1.0/pyproject.toml +43 -0
- tranquillity-0.1.0/settings/settings.py +5 -0
- tranquillity-0.1.0/setup.cfg +4 -0
- tranquillity-0.1.0/src/core/__init__.py +0 -0
- tranquillity-0.1.0/src/core/gemini.py +244 -0
- tranquillity-0.1.0/src/ctOS/help.py +21 -0
- tranquillity-0.1.0/src/ctOS/light/light.py +15 -0
- tranquillity-0.1.0/src/ctOS/light/toggle_light.py +18 -0
- tranquillity-0.1.0/src/ctOS/music/get_id_device.py +17 -0
- tranquillity-0.1.0/src/ctOS/music/get_token_tmp.py +115 -0
- tranquillity-0.1.0/src/ctOS/music/init_token.py +34 -0
- tranquillity-0.1.0/src/ctOS/music/music.py +26 -0
- tranquillity-0.1.0/src/ctOS/music/pause.py +15 -0
- tranquillity-0.1.0/src/ctOS/music/resume.py +15 -0
- tranquillity-0.1.0/src/ctOS/start/init.py +125 -0
- tranquillity-0.1.0/src/ctOS/start/start.py +61 -0
- tranquillity-0.1.0/src/subsystem/autonome_sys/autonome.py +8 -0
- tranquillity-0.1.0/src/subsystem/create_event.py +47 -0
- tranquillity-0.1.0/src/subsystem/get_creds.py +30 -0
- tranquillity-0.1.0/src/subsystem/get_email.py +101 -0
- tranquillity-0.1.0/src/subsystem/list_event.py +65 -0
- tranquillity-0.1.0/src/subsystem/notification_system.py +17 -0
- tranquillity-0.1.0/src/subsystem/search_web.py +48 -0
- tranquillity-0.1.0/src/tranquillity.py +10 -0
- tranquillity-0.1.0/tranquillity.egg-info/PKG-INFO +23 -0
- tranquillity-0.1.0/tranquillity.egg-info/SOURCES.txt +32 -0
- tranquillity-0.1.0/tranquillity.egg-info/dependency_links.txt +1 -0
- tranquillity-0.1.0/tranquillity.egg-info/entry_points.txt +2 -0
- tranquillity-0.1.0/tranquillity.egg-info/requires.txt +21 -0
- tranquillity-0.1.0/tranquillity.egg-info/top_level.txt +4 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tranquillity
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Provides-Extra: app
|
|
5
|
+
Requires-Dist: opencv-contrib-python; extra == "app"
|
|
6
|
+
Requires-Dist: requests; extra == "app"
|
|
7
|
+
Requires-Dist: ollama; extra == "app"
|
|
8
|
+
Requires-Dist: piper-tts; extra == "app"
|
|
9
|
+
Requires-Dist: vosk; extra == "app"
|
|
10
|
+
Requires-Dist: rapidfuzz; extra == "app"
|
|
11
|
+
Requires-Dist: sounddevice; extra == "app"
|
|
12
|
+
Requires-Dist: pyyaml; extra == "app"
|
|
13
|
+
Requires-Dist: spacy; extra == "app"
|
|
14
|
+
Requires-Dist: rich; extra == "app"
|
|
15
|
+
Requires-Dist: python-dotenv; extra == "app"
|
|
16
|
+
Requires-Dist: click; extra == "app"
|
|
17
|
+
Requires-Dist: google-generativeai; extra == "app"
|
|
18
|
+
Requires-Dist: google-genai; extra == "app"
|
|
19
|
+
Requires-Dist: google-api-python-client; extra == "app"
|
|
20
|
+
Requires-Dist: google-auth-httplib2; extra == "app"
|
|
21
|
+
Requires-Dist: google-auth-oauthlib; extra == "app"
|
|
22
|
+
Requires-Dist: html2text; extra == "app"
|
|
23
|
+
Requires-Dist: feedparser; extra == "app"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Tranquillity
|
|
2
|
+
|
|
3
|
+
What is Tranquillity?
|
|
4
|
+
|
|
5
|
+
Tranquillity is a project that aims to recreate a personal assistant with GLaDOS AI voice, capable of assisting with any task on a PC.
|
|
6
|
+
|
|
7
|
+
Inspired by the famous AI from the video game Portal 2, as well as projects already created throughout the Internet.
|
|
8
|
+
|
|
9
|
+
<img src="https://github.com/user-attachments/assets/1f69f6fd-e8fd-4431-956d-32d97b91bb56"
|
|
10
|
+
alt="Picture"
|
|
11
|
+
width="800"
|
|
12
|
+
height="600"
|
|
13
|
+
style="display: block; margin: 10" />
|
|
14
|
+
|
|
15
|
+
***
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
|
|
19
|
+
* **Personal AI Assistant** with GLaDOS voice synthesis (TTS).
|
|
20
|
+
* **Voice Recognition** - French language support with rapid command parsing.
|
|
21
|
+
* **Music Control** - Spotify integration (pause, resume, device management).
|
|
22
|
+
* **Lighting Control** - Smart light management.
|
|
23
|
+
* **Discord Integration** - Send messages to Discord directly from commands.
|
|
24
|
+
* **ctOS CLI** - Custom command-line interface created specifically for Tranquillity.
|
|
25
|
+
* **Voice-to-Voice LLM** - Direct voice conversation with AI processing.
|
|
26
|
+
|
|
27
|
+
***
|
|
28
|
+
|
|
29
|
+
Planned Features:
|
|
30
|
+
|
|
31
|
+
* Enhanced gesture recognition and commands.
|
|
32
|
+
* Extended camera vision capabilities.
|
|
33
|
+
* Additional smart home integrations.
|
|
34
|
+
* More language support.
|
|
35
|
+
* And much more.
|
|
36
|
+
|
|
37
|
+
***
|
|
38
|
+
|
|
39
|
+
Development / Usage:
|
|
40
|
+
|
|
41
|
+
For Linux or Windows:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
./env/bin/python -m pip install -r requirement.txt
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For NixOS:
|
|
48
|
+
|
|
49
|
+
Requires devenv
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
devenv init
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
then
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
devenv start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python ctOS.py --init
|
|
65
|
+
python ctOS.py --start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Docker:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker build -f docker/Dockerfile -t tranquillity:local .
|
|
72
|
+
TRANQUILLITY_IMAGE=tranquillity:local python docker_launcher.py --help
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `tranquillity` command from `pyproject.toml` runs `docker_launcher.py` on the host. The Docker image itself runs `ctOS.py` and forwards the arguments supplied to the launcher.
|
|
77
|
+
|
|
78
|
+
***
|
|
79
|
+
|
|
80
|
+
Inspiration:
|
|
81
|
+
|
|
82
|
+
This project wouldn't exist without inspiration and help.
|
|
83
|
+
The project is primarily educational, and here are the repositories that helped me:
|
|
84
|
+
|
|
85
|
+
* https://github.com/dnhkng/GLaDOS
|
|
86
|
+
|
|
87
|
+
***
|
|
88
|
+
|
|
89
|
+
I welcome any help!
|
|
90
|
+
|
|
91
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from src.ctOS.start.init import demo
|
|
2
|
+
from src.ctOS.start.start import start
|
|
3
|
+
from src.ctOS.help import help
|
|
4
|
+
import sys
|
|
5
|
+
import asyncio
|
|
6
|
+
from src.subsystem.notification_system import send_discord_message
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from src.tranquillity import init as tranquillity
|
|
9
|
+
from src.ctOS.music.music import music_command
|
|
10
|
+
from src.ctOS.music.init_token import init
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
nb = len(sys.argv)
|
|
15
|
+
|
|
16
|
+
if nb <= 1:
|
|
17
|
+
help()
|
|
18
|
+
return
|
|
19
|
+
if sys.argv[1] == "--demo":
|
|
20
|
+
demo()
|
|
21
|
+
start()
|
|
22
|
+
return
|
|
23
|
+
if sys.argv[1] == "--music":
|
|
24
|
+
music_command()
|
|
25
|
+
return
|
|
26
|
+
if sys.argv[1] == "--send":
|
|
27
|
+
if sys.argv[2] == None:
|
|
28
|
+
return 84
|
|
29
|
+
asyncio.run(send_discord_message(sys.argv[2]))
|
|
30
|
+
return
|
|
31
|
+
if sys.argv[1] == "--init":
|
|
32
|
+
init()
|
|
33
|
+
return
|
|
34
|
+
if sys.argv[1] == "--start":
|
|
35
|
+
tranquillity()
|
|
36
|
+
return
|
|
37
|
+
if sys.argv[1] == "--help" or "-h":
|
|
38
|
+
help()
|
|
39
|
+
else:
|
|
40
|
+
return 84
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
IMAGE_ENV = "TRANQUILLITY_IMAGE"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> int:
|
|
11
|
+
image = os.environ.get(IMAGE_ENV)
|
|
12
|
+
if not image:
|
|
13
|
+
print(
|
|
14
|
+
f"{IMAGE_ENV} n'est pas defini. Exemple: "
|
|
15
|
+
"export TRANQUILLITY_IMAGE=ghcr.io/mon-compte/tranquillity:latest",
|
|
16
|
+
file=sys.stderr,
|
|
17
|
+
)
|
|
18
|
+
return 2
|
|
19
|
+
|
|
20
|
+
if shutil.which("docker") is None:
|
|
21
|
+
print("Docker est requis mais la commande docker est introuvable.", file=sys.stderr)
|
|
22
|
+
return 127
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
subprocess.run(["docker", "info"], check=True)
|
|
26
|
+
subprocess.run(["docker", "pull", image], check=True)
|
|
27
|
+
return subprocess.run(
|
|
28
|
+
["docker", "run", "--rm", "-it", "--name", "tranquillity", image, *sys.argv[1:]]
|
|
29
|
+
).returncode
|
|
30
|
+
except subprocess.CalledProcessError as error:
|
|
31
|
+
return error.returncode
|
|
32
|
+
except KeyboardInterrupt:
|
|
33
|
+
return 130
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools >= 77.0.3"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "tranquillity"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
app = [
|
|
13
|
+
"opencv-contrib-python",
|
|
14
|
+
"requests",
|
|
15
|
+
"ollama",
|
|
16
|
+
"piper-tts",
|
|
17
|
+
"vosk",
|
|
18
|
+
"rapidfuzz",
|
|
19
|
+
"sounddevice",
|
|
20
|
+
"pyyaml",
|
|
21
|
+
"spacy",
|
|
22
|
+
"rich",
|
|
23
|
+
"python-dotenv",
|
|
24
|
+
"click",
|
|
25
|
+
"google-generativeai",
|
|
26
|
+
"google-genai",
|
|
27
|
+
"google-api-python-client",
|
|
28
|
+
"google-auth-httplib2",
|
|
29
|
+
"google-auth-oauthlib",
|
|
30
|
+
"html2text",
|
|
31
|
+
"feedparser",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
tranquillity = "docker_launcher:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
py-modules = ["ctOS", "docker_launcher"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["."]
|
|
42
|
+
include = ["src*", "settings*"]
|
|
43
|
+
namespaces = true
|
|
File without changes
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import sounddevice as sd
|
|
4
|
+
import queue
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import wave
|
|
8
|
+
import threading
|
|
9
|
+
import shlex
|
|
10
|
+
from piper import PiperVoice
|
|
11
|
+
from settings.settings import IA_NAME, VOICE_PATH, MODEL_PATH, SAMPLE_RATE, BLOCK_SIZE
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
import re
|
|
14
|
+
from vosk import Model, KaldiRecognizer, SetLogLevel
|
|
15
|
+
import json
|
|
16
|
+
import requests
|
|
17
|
+
from src.subsystem.search_web import search_web
|
|
18
|
+
from src.subsystem.get_email import get_email
|
|
19
|
+
from google import genai
|
|
20
|
+
from google.genai import types
|
|
21
|
+
from src.subsystem.create_event import create_event
|
|
22
|
+
from src.subsystem.list_event import list_calendar
|
|
23
|
+
|
|
24
|
+
load_dotenv()
|
|
25
|
+
|
|
26
|
+
recording_enabled = True
|
|
27
|
+
audio_queue = queue.Queue()
|
|
28
|
+
|
|
29
|
+
def callback(indata, frames, time, status):
|
|
30
|
+
global recording_enabled
|
|
31
|
+
if status:
|
|
32
|
+
print(status, file=sys.stderr)
|
|
33
|
+
if recording_enabled:
|
|
34
|
+
audio_queue.put(bytes(indata))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def clean_for_tts(text: str) -> str:
|
|
38
|
+
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
|
39
|
+
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
|
40
|
+
text = re.sub(r'[*#`_]', '', text)
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
def player_worker(q: "queue.Queue[str | None]"):
|
|
44
|
+
global recording_enabled
|
|
45
|
+
while True:
|
|
46
|
+
path = q.get()
|
|
47
|
+
if path is None:
|
|
48
|
+
return
|
|
49
|
+
recording_enabled = False
|
|
50
|
+
subprocess.run(["mpv", "--no-terminal", "--really-quiet", path])
|
|
51
|
+
recording_enabled = True
|
|
52
|
+
try:
|
|
53
|
+
os.remove(path)
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clear_screen():
|
|
59
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
60
|
+
|
|
61
|
+
def get_api_key():
|
|
62
|
+
api_key = os.getenv("GEMINI_API_KEY")
|
|
63
|
+
if api_key:
|
|
64
|
+
return api_key
|
|
65
|
+
|
|
66
|
+
print("=== Configuration de la clé API Gemini ===")
|
|
67
|
+
print("Vous pouvez également définir la variable d'environnement GEMINI_API_KEY pour éviter cette étape.\n")
|
|
68
|
+
while True:
|
|
69
|
+
api_key = input("Veuillez entrer votre clé API Google Gemini : ").strip()
|
|
70
|
+
if api_key:
|
|
71
|
+
return api_key
|
|
72
|
+
print("La clé ne peut pas être vide. Veuillez réessayer.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def tts_worker(text_q, out_q, voice):
|
|
76
|
+
while True:
|
|
77
|
+
fragment = text_q.get()
|
|
78
|
+
if fragment is None:
|
|
79
|
+
out_q.put(None)
|
|
80
|
+
return
|
|
81
|
+
fd, path = tempfile.mkstemp(prefix="tts_", suffix=".wav")
|
|
82
|
+
os.close(fd)
|
|
83
|
+
with wave.open(path, "wb") as wav_file:
|
|
84
|
+
voice.synthesize_wav(fragment, wav_file)
|
|
85
|
+
out_q.put(path)
|
|
86
|
+
|
|
87
|
+
def detect_command(response_text):
|
|
88
|
+
resultat = re.search(r"(?mi)^command:\s*(.+)$", response_text)
|
|
89
|
+
|
|
90
|
+
if resultat:
|
|
91
|
+
commande = resultat.group(1).strip()
|
|
92
|
+
return resultat.start(), commande
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def execute_command(commande_raw):
|
|
96
|
+
if commande_raw:
|
|
97
|
+
try:
|
|
98
|
+
try:
|
|
99
|
+
args = shlex.split(commande_raw)
|
|
100
|
+
subprocess.run(args, shell=False)
|
|
101
|
+
except ValueError:
|
|
102
|
+
subprocess.run(commande_raw, shell=True)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def gemini():
|
|
107
|
+
clear_screen()
|
|
108
|
+
print("Initialisation Tranquillity")
|
|
109
|
+
|
|
110
|
+
api_key = get_api_key()
|
|
111
|
+
model_name = IA_NAME
|
|
112
|
+
|
|
113
|
+
print("Initialisation de la personalité")
|
|
114
|
+
|
|
115
|
+
with open("settings/personality.txt", "r", encoding="utf-8") as f:
|
|
116
|
+
content = f.read()
|
|
117
|
+
system_instruction = content
|
|
118
|
+
if not system_instruction:
|
|
119
|
+
system_instruction = "Tu es un assistant virtuel utile, amical et précis. Réponds toujours en français."
|
|
120
|
+
print(f"-> Contexte par défaut appliqué : \"{system_instruction}\"")
|
|
121
|
+
else:
|
|
122
|
+
print(f"-> Contexte personnalisé appliqué avec succès !")
|
|
123
|
+
|
|
124
|
+
print("\nInitialisation du modèle...")
|
|
125
|
+
|
|
126
|
+
client = genai.Client(api_key=api_key)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
chat = client.chats.create(
|
|
130
|
+
model=model_name,
|
|
131
|
+
config=types.GenerateContentConfig(
|
|
132
|
+
system_instruction=system_instruction,
|
|
133
|
+
tools=[search_web, get_email, list_calendar, create_event]
|
|
134
|
+
),
|
|
135
|
+
history=[]
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Erreur lors de l'initialisation du modèle : {e}")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
voice = PiperVoice.load(VOICE_PATH)
|
|
142
|
+
print("voice init success")
|
|
143
|
+
|
|
144
|
+
print("Initialisation VOSK")
|
|
145
|
+
print("VOSK load")
|
|
146
|
+
SetLogLevel(-1)
|
|
147
|
+
|
|
148
|
+
model = Model(MODEL_PATH)
|
|
149
|
+
rec = KaldiRecognizer(model, SAMPLE_RATE)
|
|
150
|
+
|
|
151
|
+
clear_screen()
|
|
152
|
+
print("==================================================")
|
|
153
|
+
print(" 💬 CHAT INITIALISÉ AVEC SUCCÈS ")
|
|
154
|
+
print("==================================================")
|
|
155
|
+
print(f"Modèle utilisé : {model_name}")
|
|
156
|
+
print("--------------------------------------------------")
|
|
157
|
+
print("Instructions :")
|
|
158
|
+
print(" - Tapez votre message et appuyez sur Entrée.")
|
|
159
|
+
print(" - Écrivez 'exit', 'quit' ou 'quitter' pour fermer le chat.")
|
|
160
|
+
print(" - Écrivez 'clear' pour vider l'écran du terminal.")
|
|
161
|
+
print("==================================================\n")
|
|
162
|
+
|
|
163
|
+
audio_q: "queue.Queue[str | None]" = queue.Queue()
|
|
164
|
+
tts_text_q: "queue.Queue[str | None]" = queue.Queue()
|
|
165
|
+
|
|
166
|
+
t_tts = threading.Thread(target=tts_worker, args=(tts_text_q, audio_q, voice), daemon=True)
|
|
167
|
+
t_tts.start()
|
|
168
|
+
t = threading.Thread(target=player_worker, args=(audio_q,), daemon=True)
|
|
169
|
+
t.start()
|
|
170
|
+
|
|
171
|
+
one_use = False
|
|
172
|
+
print("En écoute...")
|
|
173
|
+
with sd.RawInputStream(
|
|
174
|
+
samplerate=SAMPLE_RATE,
|
|
175
|
+
blocksize=BLOCK_SIZE,
|
|
176
|
+
dtype="int16",
|
|
177
|
+
channels=1,
|
|
178
|
+
callback=callback,
|
|
179
|
+
):
|
|
180
|
+
while True:
|
|
181
|
+
try:
|
|
182
|
+
data = audio_queue.get()
|
|
183
|
+
audio_q: "queue.Queue[str | None]" = queue.Queue()
|
|
184
|
+
if rec.AcceptWaveform(data) and one_use == False:
|
|
185
|
+
one_use = True
|
|
186
|
+
result = json.loads(rec.Result())
|
|
187
|
+
user_input = result.get("text", "").strip()
|
|
188
|
+
if not user_input:
|
|
189
|
+
one_use = False
|
|
190
|
+
continue
|
|
191
|
+
if user_input.lower() in ['exit', 'quit', 'quitter']:
|
|
192
|
+
print("\n Assistant : Au revoir ! Passez une excellente journée.")
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if user_input.lower() == 'clear':
|
|
196
|
+
clear_screen()
|
|
197
|
+
print("==================================================")
|
|
198
|
+
print(" CONVERSATION EN COURS ")
|
|
199
|
+
print("================================================--\n")
|
|
200
|
+
one_use = False
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
print("Vous", user_input)
|
|
204
|
+
print(" Assistant : ", end="", flush=True)
|
|
205
|
+
response = chat.send_message(user_input)
|
|
206
|
+
|
|
207
|
+
streamed_text = response.text
|
|
208
|
+
print(streamed_text, flush=True)
|
|
209
|
+
|
|
210
|
+
command_start = None
|
|
211
|
+
command_match = detect_command(streamed_text)
|
|
212
|
+
if command_match:
|
|
213
|
+
command_start, _ = command_match
|
|
214
|
+
spoken_text = streamed_text[:command_start]
|
|
215
|
+
else:
|
|
216
|
+
spoken_text = streamed_text
|
|
217
|
+
|
|
218
|
+
clean_tts = clean_for_tts(spoken_text)
|
|
219
|
+
if clean_tts.strip():
|
|
220
|
+
tts_text_q.put(clean_tts)
|
|
221
|
+
|
|
222
|
+
print("\n")
|
|
223
|
+
|
|
224
|
+
if command_start is not None:
|
|
225
|
+
_, commande = detect_command(streamed_text)
|
|
226
|
+
if commande:
|
|
227
|
+
print(f"Commande détectée : {commande}")
|
|
228
|
+
run_command = threading.Thread(target=execute_command, args=(commande,), daemon=True)
|
|
229
|
+
run_command.start()
|
|
230
|
+
one_use = False
|
|
231
|
+
|
|
232
|
+
except KeyboardInterrupt:
|
|
233
|
+
print("\n\n Assistant : Chat interrompu. Au revoir !")
|
|
234
|
+
audio_q.put(None)
|
|
235
|
+
t.join()
|
|
236
|
+
break
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f"\nUne erreur est survenue lors de la communication avec l'API : {e}\n")
|
|
239
|
+
audio_q.put(None)
|
|
240
|
+
t.join()
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
gemini()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def help():
|
|
2
|
+
print("==================================================")
|
|
3
|
+
print(" Bienvenue dans ctOS - CLI Tranquillity")
|
|
4
|
+
print("==================================================\n")
|
|
5
|
+
print("USAGE :")
|
|
6
|
+
print(" ctOS [commande]")
|
|
7
|
+
print(" ctOS [option]\n")
|
|
8
|
+
print("COMMANDES PRINCIPALES :\n")
|
|
9
|
+
print(" --start Lance le programme principal Tranquillity")
|
|
10
|
+
print("GESTION MUSICALE :\n")
|
|
11
|
+
print(" --music pause Met en pause la musique en cours")
|
|
12
|
+
print(" --music resume Reprend la musique en pause")
|
|
13
|
+
print(" --music device Affiche et gere les appareils audio connectes\n")
|
|
14
|
+
print("CONTROLE DOMOTIQUE :\n")
|
|
15
|
+
print(" --light toggle Active/desactive les lumieres connectees\n")
|
|
16
|
+
print("OUTILS ET CONFIGURATION :\n")
|
|
17
|
+
print(" --init Initialise le token Spotify et vérification")
|
|
18
|
+
print(" --send [message] Envoie un message via Discord\n")
|
|
19
|
+
print("AUTRES :\n")
|
|
20
|
+
print(" --help, -h Affiche ce message d'aide\n")
|
|
21
|
+
print("==================================================\n")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from src.ctOS.light.toggle_light import toggle_light
|
|
4
|
+
|
|
5
|
+
def light_command():
|
|
6
|
+
nb = len(sys.argv)
|
|
7
|
+
|
|
8
|
+
if nb <= 2:
|
|
9
|
+
print("no argument give. Please give word")
|
|
10
|
+
return
|
|
11
|
+
if sys.argv[2] == "toggle":
|
|
12
|
+
toggle_light()
|
|
13
|
+
return
|
|
14
|
+
else:
|
|
15
|
+
print("no argument give. Please give word")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
FIFO_PATH = "/tmp/kitty_shell_input"
|
|
5
|
+
CMD_LIGHT = "./ecc-api --toggle"
|
|
6
|
+
|
|
7
|
+
def toggle_light():
|
|
8
|
+
if not os.path.exists(FIFO_PATH):
|
|
9
|
+
print(f"Sub-Agent: FIFO introuvable: {FIFO_PATH}")
|
|
10
|
+
return
|
|
11
|
+
with open(FIFO_PATH, "w") as fifo:
|
|
12
|
+
fifo.write(CMD_LIGHT + "\n")
|
|
13
|
+
subprocess.run(CMD_LIGHT)
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
def toggle_light():
|
|
17
|
+
subprocess.run(CMD_LIGHT)
|
|
18
|
+
return
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from src.ctOS.music.get_token_tmp import get_token
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
def get_id_device():
|
|
8
|
+
access_token = get_token()
|
|
9
|
+
url = "https://api.spotify.com/v1/me/player/devices"
|
|
10
|
+
header = {"Authorization": "Bearer " + access_token}
|
|
11
|
+
|
|
12
|
+
result = requests.get(url, headers=header)
|
|
13
|
+
try:
|
|
14
|
+
body = result.json()
|
|
15
|
+
except Exception:
|
|
16
|
+
body = result.text
|
|
17
|
+
print(body)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import webbrowser
|
|
5
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
6
|
+
from urllib.parse import urlencode, urlparse, parse_qs
|
|
7
|
+
from requests import post
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
CLIENT_ID = os.getenv("TOKEN_PUBLIC")
|
|
13
|
+
CLIENT_SECRET = os.getenv("TOKEN_PRIVATE")
|
|
14
|
+
REDIRECT_URI = "http://127.0.0.1:8888/callback"
|
|
15
|
+
SCOPES = "user-read-playback-state user-modify-playback-state"
|
|
16
|
+
TOKEN_FILE = os.path.join(os.path.dirname(__file__), ".spotify_token.json")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _basic_auth_header():
|
|
20
|
+
auth_string = f"{CLIENT_ID}:{CLIENT_SECRET}".encode("utf-8")
|
|
21
|
+
return "Basic " + base64.b64encode(auth_string).decode("utf-8")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
25
|
+
code = None
|
|
26
|
+
|
|
27
|
+
def do_GET(self):
|
|
28
|
+
query = parse_qs(urlparse(self.path).query)
|
|
29
|
+
_CallbackHandler.code = query.get("code", [None])[0]
|
|
30
|
+
self.send_response(200)
|
|
31
|
+
self.send_header("Content-type", "text/html")
|
|
32
|
+
self.end_headers()
|
|
33
|
+
self.wfile.write(b"<h1>You can close this.</h1>")
|
|
34
|
+
|
|
35
|
+
def log_message(self, format, *args):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _request_authorization_code():
|
|
40
|
+
params = {
|
|
41
|
+
"client_id": CLIENT_ID,
|
|
42
|
+
"response_type": "code",
|
|
43
|
+
"redirect_uri": REDIRECT_URI,
|
|
44
|
+
"scope": SCOPES,
|
|
45
|
+
}
|
|
46
|
+
auth_url = "https://accounts.spotify.com/authorize?" + urlencode(params)
|
|
47
|
+
webbrowser.open(auth_url)
|
|
48
|
+
|
|
49
|
+
server = HTTPServer(("127.0.0.1", 8888), _CallbackHandler)
|
|
50
|
+
server.handle_request()
|
|
51
|
+
return _CallbackHandler.code
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _exchange_code_for_tokens(code):
|
|
55
|
+
url = "https://accounts.spotify.com/api/token"
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": _basic_auth_header(),
|
|
58
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
59
|
+
}
|
|
60
|
+
data = {
|
|
61
|
+
"grant_type": "authorization_code",
|
|
62
|
+
"code": code,
|
|
63
|
+
"redirect_uri": REDIRECT_URI,
|
|
64
|
+
}
|
|
65
|
+
result = post(url, headers=headers, data=data)
|
|
66
|
+
result.raise_for_status()
|
|
67
|
+
return result.json()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _refresh_access_token(refresh_token):
|
|
71
|
+
url = "https://accounts.spotify.com/api/token"
|
|
72
|
+
headers = {
|
|
73
|
+
"Authorization": _basic_auth_header(),
|
|
74
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75
|
+
}
|
|
76
|
+
data = {
|
|
77
|
+
"grant_type": "refresh_token",
|
|
78
|
+
"refresh_token": refresh_token,
|
|
79
|
+
}
|
|
80
|
+
result = post(url, headers=headers, data=data)
|
|
81
|
+
result.raise_for_status()
|
|
82
|
+
return result.json()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _save_tokens(tokens):
|
|
86
|
+
with open(TOKEN_FILE, "w") as f:
|
|
87
|
+
json.dump(tokens, f)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_tokens():
|
|
91
|
+
if not os.path.exists(TOKEN_FILE):
|
|
92
|
+
return None
|
|
93
|
+
with open(TOKEN_FILE) as f:
|
|
94
|
+
return json.load(f)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_token():
|
|
98
|
+
if not CLIENT_ID or not CLIENT_SECRET:
|
|
99
|
+
raise RuntimeError("TOKEN_PUBLIC et TOKEN_PRIVATE doivent etre definis dans l'environnement")
|
|
100
|
+
|
|
101
|
+
tokens = _load_tokens()
|
|
102
|
+
|
|
103
|
+
if tokens and "refresh_token" in tokens:
|
|
104
|
+
new_tokens = _refresh_access_token(tokens["refresh_token"])
|
|
105
|
+
new_tokens.setdefault("refresh_token", tokens["refresh_token"])
|
|
106
|
+
_save_tokens(new_tokens)
|
|
107
|
+
return new_tokens["access_token"]
|
|
108
|
+
|
|
109
|
+
code = _request_authorization_code()
|
|
110
|
+
if not code:
|
|
111
|
+
raise RuntimeError("Aucun code recu, autorisation echouee")
|
|
112
|
+
|
|
113
|
+
tokens = _exchange_code_for_tokens(code)
|
|
114
|
+
_save_tokens(tokens)
|
|
115
|
+
return tokens["access_token"]
|