tranquillity 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ctOS.py ADDED
@@ -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()
docker_launcher.py ADDED
@@ -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())
settings/settings.py ADDED
@@ -0,0 +1,5 @@
1
+ IA_NAME = "gemini-3.5-flash-lite"
2
+ VOICE_PATH = "model/tts/glados/fr_FR-glados-medium.onnx"
3
+ MODEL_PATH = "model/vosk/vosk-model-small-fr/vosk-model-fr-0.22"
4
+ SAMPLE_RATE = 16000
5
+ BLOCK_SIZE = 4000
src/core/__init__.py ADDED
File without changes
src/core/gemini.py ADDED
@@ -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()
src/ctOS/help.py ADDED
@@ -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"]
@@ -0,0 +1,34 @@
1
+ import os
2
+ from pathlib import Path
3
+ from src.ctOS.music.get_token_tmp import get_token
4
+ import time
5
+
6
+ PATH_TOKEN = "src/music/.spotify_token.json"
7
+
8
+ def init():
9
+ p = Path(".env")
10
+ if not p.exists():
11
+ print("Pas de .env detecter. Veuillez en créer un")
12
+ return 84
13
+
14
+ token_public = os.getenv("TOKEN_PUBLIC")
15
+ token_private = os.getenv("TOKEN_PRIVATE")
16
+ if not token_public or not token_private:
17
+ print("Pas de token detecter. Veuillez vous referer a la doc pour pouvoir en obtenir")
18
+ time.sleep(2)
19
+ return 84
20
+
21
+ p = Path(PATH_TOKEN)
22
+ if not p.exists():
23
+ print("Premier utilisation detecter. Redirection vers la connection spotify")
24
+ time.sleep(2)
25
+ get_token()
26
+
27
+ token_discord = os.getenv("API_KEY")
28
+ if not token_discord:
29
+ print("Pas de Clef API pour connecter Tranquility au bot discord. Si vous souhaitez l'utiliser inscrivez vous au bot discord et mettez la clef api dans le .env")
30
+ else:
31
+ print("Clef API est détécter. L'integration discord est donc disponible.")
32
+ time.sleep(2)
33
+ print("Tous est désormais initialisé. Veuillez faire ctOS --start")
34
+ time.sleep(2)
@@ -0,0 +1,26 @@
1
+ import requests
2
+ import json
3
+ import sys
4
+ from src.ctOS.music.get_token_tmp import get_token
5
+ import requests
6
+ from src.ctOS.music.get_id_device import get_id_device
7
+ from src.ctOS.music.pause import pause
8
+ from src.ctOS.music.resume import resume
9
+
10
+ def music_command():
11
+ nb = len(sys.argv)
12
+
13
+ if nb <= 2:
14
+ print("no argument give. Please give word")
15
+ return
16
+ if sys.argv[2] == "device":
17
+ get_id_device()
18
+ return
19
+ if sys.argv[2] == "pause":
20
+ pause()
21
+ return
22
+ if sys.argv[2] == "resume":
23
+ resume()
24
+ return
25
+ else:
26
+ print("no argument give. Please give word")
@@ -0,0 +1,15 @@
1
+ import requests
2
+ from src.ctOS.music.get_token_tmp import get_token
3
+
4
+ def pause():
5
+ access_token = get_token()
6
+ url = "https://api.spotify.com/v1/me/player/pause"
7
+ header = {"Authorization": "Bearer " + access_token}
8
+
9
+ result = requests.put(url, headers=header)
10
+ try:
11
+ body = result.json()
12
+ except Exception:
13
+ body = result.text
14
+ print(body)
15
+ print(result.status_code)
@@ -0,0 +1,15 @@
1
+ import requests
2
+ from src.ctOS.music.get_token_tmp import get_token
3
+
4
+ def resume():
5
+ access_token = get_token()
6
+ url = "https://api.spotify.com/v1/me/player/play"
7
+ header = {"Authorization": "Bearer " + access_token, "Content-Type": "application/json"}
8
+ data = {}
9
+ result = requests.put(url, headers=header, json=data)
10
+ try:
11
+ body = result.json()
12
+ except Exception:
13
+ body = result.text
14
+ print(body)
15
+ print(result.status_code)
src/ctOS/start/init.py ADDED
@@ -0,0 +1,125 @@
1
+ import time
2
+ import random
3
+ from collections import deque
4
+ from rich.console import Console
5
+ from rich.layout import Layout
6
+ from rich.live import Live
7
+ from rich.text import Text
8
+ from rich.align import Align
9
+ from rich.panel import Panel
10
+ from pathlib import Path
11
+ import os
12
+ from dotenv import load_dotenv
13
+
14
+ load_dotenv()
15
+
16
+ console = Console()
17
+ GREEN = "bold spring_green2"
18
+ DIM_GREEN = "spring_green4"
19
+
20
+ BOOT_LINES = [
21
+ "[INIT] Check env is created",
22
+ "[OK ] Env check",
23
+ "[INIT] Check env variable",
24
+ "[OK ] Variable good",
25
+ "[INIT] Check spotify section",
26
+ "[OK ] Token is good",
27
+ "[INIT] Check other command",
28
+ "[OK ] Other command good",
29
+ "[INIT] Start cli...",
30
+ ]
31
+
32
+ SPACE_LINE = [
33
+ " "
34
+ ]
35
+
36
+ WAITING_LINE = [
37
+ "."
38
+ ".."
39
+ "..."
40
+ ]
41
+
42
+ START_LINE = [
43
+ "Hello from ctOS"
44
+ ]
45
+
46
+ ERROR_LINE = "Error detected. First time to use ? Use ctOS --init"
47
+
48
+ def make_layout() -> Layout:
49
+ layout = Layout()
50
+ layout.split_column(
51
+ Layout(name="main", ratio=2),
52
+ Layout(name="bottom", size=9),
53
+ )
54
+ layout["bottom"].split_row(
55
+ Layout(name="bottom_left"),
56
+ )
57
+ return layout
58
+
59
+ def render_center(percent: int, width: int = 40) -> Align:
60
+ filled = int(width * percent / 100)
61
+ empty = width - filled
62
+ bar_str = "█" * filled + "░" * empty
63
+ text = Text(f"{bar_str} {percent:3d}%", style=GREEN, justify="center")
64
+ return Align.center(text, vertical="middle")
65
+
66
+ def render_status(history: deque) -> Panel:
67
+ body = Text("\n".join(history), style=DIM_GREEN)
68
+ return Panel(body, border_style=DIM_GREEN)
69
+
70
+ def check_file_env():
71
+ p = Path(".env")
72
+ if p.exists():
73
+ return 0
74
+ else:
75
+ return 84
76
+
77
+ def check_token():
78
+ p = Path("src/music/.spotify_token.json")
79
+ if p.exists():
80
+ return 0
81
+ else:
82
+ return 84
83
+
84
+ def check_variable():
85
+ token_public = os.getenv("TOKEN_PUBLIC")
86
+ token_private = os.getenv("TOKEN_PRIVATE")
87
+ if not token_public or not token_private:
88
+ return 84
89
+ else:
90
+ return 0
91
+
92
+ STEPS = [
93
+ ("[INIT] Check env is created", None),
94
+ ("[OK ] Env check", check_file_env),
95
+ ("[INIT] Check env variable", None),
96
+ ("[OK ] Variable good", check_variable),
97
+ ("[INIT] Check spotify section", None),
98
+ ("[OK ] Token is good", check_token),
99
+ ("[INIT] Check other command", None),
100
+ ("[OK ] Other command good", None),
101
+ ("[INIT] Start cli...", None),
102
+ ]
103
+
104
+ def demo():
105
+ layout = make_layout()
106
+ history = deque(maxlen=6)
107
+
108
+ with Live(layout, console=console, refresh_per_second=20, screen=True):
109
+ for i, (label, check) in enumerate(STEPS):
110
+ history.append(label)
111
+
112
+ if check is not None:
113
+ r = check()
114
+ if r == 84:
115
+ history.append(ERROR_LINE)
116
+ time.sleep(2)
117
+ return 84
118
+
119
+ percent = ((i + 1) * 100) // len(STEPS)
120
+ layout["main"].update(render_center(percent))
121
+ layout["bottom_left"].update(render_status(history))
122
+ time.sleep(1)
123
+
124
+ if __name__ == "__main__":
125
+ demo()
@@ -0,0 +1,61 @@
1
+
2
+ import requests
3
+ import time
4
+ import random
5
+ from collections import deque
6
+ from rich.console import Console
7
+ from rich.layout import Layout
8
+ from rich.live import Live
9
+ from rich.text import Text
10
+ from rich.align import Align
11
+ from rich.panel import Panel
12
+
13
+ console = Console()
14
+ GREEN = "bold spring_green2"
15
+ DIM_GREEN = "spring_green4"
16
+
17
+ BOOT_LINES = [
18
+ "[INIT] Loading all module...",
19
+ "[NET ] Establishing uplink to backbone...",
20
+ "[OK ] All systems are onlines.",
21
+ ]
22
+
23
+ SPACE_LINE = [
24
+ " "
25
+ ]
26
+
27
+ WAITING_LINE = [
28
+ "."
29
+ ".."
30
+ "..."
31
+ ]
32
+
33
+ START_LINE = [
34
+ "Hello from ctOS"
35
+ ]
36
+
37
+ def make_layout() -> Layout:
38
+ layout = Layout()
39
+ layout.split_column(
40
+ Layout(name="main", ratio=2),
41
+ )
42
+ return layout
43
+
44
+ def render_status(history: deque) -> Panel:
45
+ body = Text("\n".join(history), style=DIM_GREEN)
46
+ return Panel(body, border_style=DIM_GREEN)
47
+
48
+ def start():
49
+ layout = make_layout()
50
+ history = deque(maxlen=6)
51
+ messages = BOOT_LINES
52
+ start = START_LINE
53
+ wait = WAITING_LINE
54
+ is_finish = False
55
+
56
+ with Live(layout, console=console, refresh_per_second=20, screen=True):
57
+ while is_finish is False:
58
+ history = None
59
+ history = deque(maxlen=6)
60
+ history.append(start[0])
61
+ layout["main"].update(render_status(history))
@@ -0,0 +1,8 @@
1
+ import os
2
+ import feedparser
3
+
4
+ def autonome_chain():
5
+ while True:
6
+ i = 1
7
+
8
+
@@ -0,0 +1,47 @@
1
+ # Refer to the Python quickstart on how to setup the environment:
2
+ # https://developers.google.com/workspace/calendar/quickstart/python
3
+ # Change the scope to 'https://www.googleapis.com/auth/calendar' and delete any
4
+ # stored credentials.
5
+ from googleapiclient.discovery import build
6
+ from src.subsystem.get_creds import get_creds
7
+
8
+ def create_event(title: str, description: str, startTime: str, endTime: str):
9
+ """
10
+ Summary:
11
+ Create a event in your google calendar
12
+ Args:
13
+ title: just a title
14
+ description: just a description
15
+ startTime: start of your event like this '2026-09-09T09:00:00-07:00'
16
+ endTime: same type of startTime
17
+ """
18
+ creds = get_creds()
19
+ service = build("calendar", "v3", credentials=creds)
20
+ event = {
21
+ 'summary': title,
22
+ 'location': 'None',
23
+ 'description': description,
24
+ 'start': {
25
+ 'dateTime': startTime,
26
+ 'timeZone': 'Europe/Paris',
27
+ },
28
+ 'end': {
29
+ 'dateTime': endTime,
30
+ 'timeZone': 'Europe/Paris',
31
+ },
32
+ 'recurrence': [
33
+ 'RRULE:FREQ=DAILY;COUNT=1'
34
+ ],
35
+ 'reminders': {
36
+ 'useDefault': False,
37
+ 'overrides': [
38
+ {'method': 'email', 'minutes': 24 * 60},
39
+ {'method': 'popup', 'minutes': 10},
40
+ ],
41
+ },
42
+ }
43
+
44
+ event = service.events().insert(calendarId='primary', body=event).execute()
45
+ print('Event created: %s' % (event.get('htmlLink')))
46
+ return "'Event created: %s' % (event.get('htmlLink'))"
47
+
@@ -0,0 +1,30 @@
1
+ import os
2
+ from google.auth.transport.requests import Request
3
+ from google.oauth2.credentials import Credentials
4
+ from google_auth_oauthlib.flow import InstalledAppFlow
5
+ from googleapiclient.discovery import build
6
+ from googleapiclient.errors import HttpError
7
+
8
+ SCOPES = ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.modify",]
9
+
10
+ def get_creds():
11
+ """
12
+ Summary:
13
+ Create or check your actual token. If not exist, Tranquillity create it
14
+ """
15
+ creds = None
16
+ if os.path.exists("token.json"):
17
+ creds = Credentials.from_authorized_user_file("token.json", SCOPES)
18
+ # If there are no (valid) credentials available, let the user log in.
19
+ if not creds or not creds.valid:
20
+ if creds and creds.expired and creds.refresh_token:
21
+ creds.refresh(Request())
22
+ else:
23
+ flow = InstalledAppFlow.from_client_secrets_file(
24
+ "./settings/credentials.json", SCOPES
25
+ )
26
+ creds = flow.run_local_server(port=0)
27
+ # Save the credentials for the next run
28
+ with open("token.json", "w") as token:
29
+ token.write(creds.to_json())
30
+ return creds
@@ -0,0 +1,101 @@
1
+ import requests
2
+ import os
3
+ from google.auth.transport.requests import Request
4
+ from google.oauth2.credentials import Credentials
5
+ from google_auth_oauthlib.flow import InstalledAppFlow
6
+ from googleapiclient.discovery import build
7
+ from googleapiclient.errors import HttpError
8
+ import base64
9
+ import html2text
10
+ from src.subsystem.get_creds import get_creds
11
+
12
+ def get_header(payload, name):
13
+ for h in payload["headers"]:
14
+ if h["name"].lower() == name.lower():
15
+ return h["value"]
16
+ return ""
17
+
18
+ def html_to_text(html_content: str) -> str:
19
+ h = html2text.HTML2Text()
20
+ h.ignore_links = True
21
+ h.ignore_images = True
22
+ h.body_width = 0
23
+ return h.handle(html_content).strip()
24
+
25
+ def get_body(payload):
26
+ plain_text = None
27
+ html_text = None
28
+
29
+ if "parts" in payload:
30
+ for part in payload["parts"]:
31
+ # gère aussi les parts imbriquées (multipart/alternative dans multipart/mixed)
32
+ if part.get("parts"):
33
+ nested = get_body(part)
34
+ if nested:
35
+ return nested
36
+ if part["mimeType"] == "text/plain" and not plain_text:
37
+ data = part["body"].get("data")
38
+ if data:
39
+ plain_text = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
40
+ elif part["mimeType"] == "text/html" and not html_text:
41
+ data = part["body"].get("data")
42
+ if data:
43
+ html_text = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
44
+ else:
45
+ data = payload.get("body", {}).get("data")
46
+ if data:
47
+ decoded = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
48
+ if payload.get("mimeType") == "text/html":
49
+ html_text = decoded
50
+ else:
51
+ plain_text = decoded
52
+
53
+ if plain_text and plain_text.strip():
54
+ return plain_text.strip()
55
+ if html_text:
56
+ return html_to_text(html_text)
57
+ return ""
58
+
59
+ def get_email(number: int):
60
+ """Shows basic usage of the Gmail API.
61
+ Lists the user's Gmail labels.
62
+ """
63
+ creds = get_creds()
64
+ try:
65
+ # Call the Gmail API and limit the result to the requested number of emails.
66
+ service = build("gmail", "v1", credentials=creds)
67
+ results = (
68
+ service.users().messages().list(
69
+ userId="me", labelIds=["INBOX"], maxResults=number
70
+ ).execute()
71
+ )
72
+ messages = results.get("messages", [])
73
+
74
+ if not messages:
75
+ print("No messages found.")
76
+ return
77
+
78
+ print("Messages:")
79
+ content = []
80
+ for message in messages:
81
+ print(f'Message ID: {message["id"]}')
82
+ msg = (
83
+ service.users().messages().get(
84
+ userId="me", id=message["id"], format="full"
85
+ ).execute()
86
+ )
87
+ subject = get_header(msg["payload"], "Subject")
88
+ date = get_header(msg["payload"], "Date")
89
+ sender = get_header(msg["payload"], "From")
90
+ body = get_body(msg["payload"])
91
+ print(sender)
92
+ print(date)
93
+ print(subject)
94
+ print(body)
95
+ content.append(f"Sender : {sender}.\n Date : {date}.\n Subject: {subject}.\n Body: {body}\n")
96
+ return "\n".join(content)
97
+
98
+
99
+ except HttpError as error:
100
+ # TODO(developer) - Handle errors from gmail API.
101
+ print(f"An error occurred: {error}")
@@ -0,0 +1,65 @@
1
+ import datetime
2
+ from google.auth.transport.requests import Request
3
+ from google.oauth2.credentials import Credentials
4
+ from google_auth_oauthlib.flow import InstalledAppFlow
5
+ from googleapiclient.discovery import build
6
+ from googleapiclient.errors import HttpError
7
+ from src.subsystem.get_creds import get_creds
8
+ from datetime import datetime as normaldatetime
9
+ from zoneinfo import ZoneInfo
10
+
11
+ # If modifying these scopes, delete the file token.json.
12
+ SCOPES = ["https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/gmail.readonly",]
13
+
14
+
15
+ def format_event_time(iso_string: str) -> str:
16
+ if "T" not in iso_string:
17
+ # événement toute la journée, pas d'heure
18
+ return iso_string
19
+ dt_utc = normaldatetime.fromisoformat(iso_string.replace("Z", "+00:00"))
20
+ dt_paris = dt_utc.astimezone(ZoneInfo("Europe/Paris"))
21
+ return dt_paris.strftime("%Y-%m-%d %H:%M")
22
+
23
+ def list_calendar(number: int):
24
+ """Shows basic usage of the Google Calendar API.
25
+ Prints the start and name of the next 10 events on the user's calendar.
26
+ """
27
+ creds = get_creds()
28
+
29
+ try:
30
+ service = build("calendar", "v3", credentials=creds)
31
+
32
+ # Call the Calendar API
33
+ now = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
34
+ print("Getting the upcoming 10 events")
35
+ events_result = (
36
+ service.events()
37
+ .list(
38
+ calendarId="primary",
39
+ timeMin=now,
40
+ maxResults=number,
41
+ singleEvents=True,
42
+ orderBy="startTime",
43
+ )
44
+ .execute()
45
+ )
46
+ events = events_result.get("items", [])
47
+
48
+ if not events:
49
+ print("No upcoming events found.")
50
+ return ""
51
+
52
+ content = []
53
+ # Prints the start and name of the next 10 events
54
+ for event in events:
55
+ start = event["start"].get("dateTime", event["start"].get("date"))
56
+ print(format_event_time(start), event["summary"])
57
+ temp_content = f"{start} - {event["summary"]}"
58
+ content.append(temp_content)
59
+ return "\n".join(content)
60
+
61
+ except HttpError as error:
62
+ print(f"An error occurred: {error}")
63
+
64
+ if __name__ == "__main__":
65
+ list_calendar(5)
@@ -0,0 +1,17 @@
1
+ import json
2
+ import os
3
+ import requests
4
+ import secrets
5
+ from pathlib import Path
6
+ import asyncio
7
+ import httpx
8
+
9
+ async def send_discord_message(text: str):
10
+ async with httpx.AsyncClient() as client:
11
+ r = await client.post(
12
+ "https://gla-dos-prod.up.railway.app/discord/say",
13
+ json={"text": text},
14
+ headers={"X-Glados-Key": os.getenv("API_KEY")},
15
+ )
16
+ r.raise_for_status()
17
+ return r.json()
@@ -0,0 +1,48 @@
1
+ import os
2
+ import requests
3
+
4
+ def search_web(query: str) -> str:
5
+ api_key = os.getenv("NEWSAPI_KEY")
6
+ if not api_key:
7
+ return "Erreur : clé NEWSAPI_KEY manquante dans les variables d'environnement."
8
+
9
+ mots_generaux = ["actualité", "actualités", "info", "infos", "news", "dernières", "nouvelles"]
10
+ is_general = any(mot in query.lower() for mot in mots_generaux)
11
+ q = "France" if is_general else query
12
+
13
+ try:
14
+ resp = requests.get(
15
+ "https://newsapi.org/v2/everything",
16
+ params={
17
+ "q": q,
18
+ "language": "fr",
19
+ "sortBy": "publishedAt",
20
+ "pageSize": 5,
21
+ "apiKey": api_key,
22
+ },
23
+ timeout=8,
24
+ )
25
+ data = resp.json()
26
+
27
+ if data.get("status") != "ok":
28
+ return f"Erreur API actualités : {data.get('message', 'inconnue')}"
29
+
30
+ articles = data.get("articles", [])
31
+ if not articles:
32
+ return "Aucun résultat trouvé pour cette recherche."
33
+
34
+ lignes = []
35
+ for a in articles:
36
+ titre = a.get("title", "").strip()
37
+ source = a.get("source", {}).get("name", "")
38
+ description = (a.get("description") or "").strip()
39
+ if titre:
40
+ ligne = f"- {titre} ({source})"
41
+ if description:
42
+ ligne += f" : {description}"
43
+ lignes.append(ligne)
44
+
45
+ return "\n".join(lignes) if lignes else "Aucun résultat exploitable trouvé."
46
+
47
+ except requests.exceptions.RequestException as e:
48
+ return f"Erreur réseau lors de la recherche : {e}"
src/tranquillity.py ADDED
@@ -0,0 +1,10 @@
1
+ import sys
2
+ from threading import Thread
3
+ from pathlib import Path
4
+ from src.core.gemini import gemini
5
+ from src.subsystem.autonome_sys.autonome import autonome_chain
6
+
7
+ def init():
8
+ autonome = Thread(target=autonome_chain, daemon=True)
9
+ autonome.start()
10
+ gemini()
@@ -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,29 @@
1
+ ctOS.py,sha256=5852cYA5lVpGJrEGc_SjoJEUCwnpbEzidbeGgFIOW_M,1032
2
+ docker_launcher.py,sha256=HosJFPTQi31-F16bSkybnNIezSQ4oczd0jURKpBQ9Nk,985
3
+ settings/settings.py,sha256=SBKpmH8fffXVM_RGimK5v7smRzOHJ2OYFjbWU1tzjZc,193
4
+ src/tranquillity.py,sha256=pPZSyQxZaQcXYTmmpJnk37yYbHUNB6IreJcqHxW9zco,267
5
+ src/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ src/core/gemini.py,sha256=osnpQTR2GiDLqu9Qcf43TfVVgO3UuPZ3qQB2GxShEW8,8301
7
+ src/ctOS/help.py,sha256=TwRGZJIzUaoDqtPMbjYHGc60fRDTyxE-N-xj9Qgb4o8,1112
8
+ src/ctOS/light/light.py,sha256=ENIpnAXPbhkngL-EaLUwxGRf9kkVOLE0DNVu3q53kdM,335
9
+ src/ctOS/light/toggle_light.py,sha256=iG78Cs8WLRjC8mYyhHwbUZNUf8odZOIWGhHt5VgxplA,410
10
+ src/ctOS/music/get_id_device.py,sha256=osRZqpmZpO80sTAUvr-ZlpgIDItcekupFhcSuakGY-o,424
11
+ src/ctOS/music/get_token_tmp.py,sha256=vZj9VeDWryg4oOrIM4lA0deHpm9l4TqXItS-30rpzcA,3316
12
+ src/ctOS/music/init_token.py,sha256=GF1K0785vT1cnxsTHtcxQH8JIp8gLFcy4RjYhnlINJ4,1181
13
+ src/ctOS/music/music.py,sha256=8Q38Zt6AW7o2RLq6XUHVwwHXazVJRcPnf9huRzfQjVc,630
14
+ src/ctOS/music/pause.py,sha256=mVilxUK8ykWZhwj6wUkrm7SQNW52vAoRvJ-ciYzGOJU,404
15
+ src/ctOS/music/resume.py,sha256=vuBsDPEvIIDzm1Z1HQDdFQGUv5ec7T8hIlkyT2EoyiM,464
16
+ src/ctOS/start/init.py,sha256=f7hCI4-r0T7x_QSegNpm-F4aaSm8MmYcWqjUAHqAuNg,3142
17
+ src/ctOS/start/start.py,sha256=TPQpgpqYM4nxy-GJkdVbEfr7hBzZvDszAZ_kqB5f0Bc,1302
18
+ src/subsystem/create_event.py,sha256=DcQMIWDgAyE5fZ6W3ARyymMIpYTXslp7d4J-MQVqtJQ,1499
19
+ src/subsystem/get_creds.py,sha256=abSccAwzhYNT7DINRXM2cvOD358i1QdSoSr9lzQanfs,1158
20
+ src/subsystem/get_email.py,sha256=vUXQjIb6NveiU78QvzTOIRqXjls2mnnZgvYMxUM7F24,3345
21
+ src/subsystem/list_event.py,sha256=mJXiG54LBTUZ4lGDYdNJcGk6frYimlMCCYpAhzpyoAM,2127
22
+ src/subsystem/notification_system.py,sha256=r1MyBn5S0EJnabGmcnOYfd-aF5GgyOOYQWWFL_hRvLs,446
23
+ src/subsystem/search_web.py,sha256=-64AejTzF0B1G8XVsLwgCcroOSSOSrWVzMpRfRi3ecI,1635
24
+ src/subsystem/autonome_sys/autonome.py,sha256=1wxQqIspCuF0w5_gOAsKDn9y8aQ-Bt5g_9QIfV4Oj00,83
25
+ tranquillity-0.1.0.dist-info/METADATA,sha256=I6H2xRytqQMnnW4VwHDQjDOw49qD04Xw-GJBjbdqtsw,899
26
+ tranquillity-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
+ tranquillity-0.1.0.dist-info/entry_points.txt,sha256=v9iaIcdDzmX2dsMrhd-Hmyf8vejSH7wRrTS_hHbdmKI,54
28
+ tranquillity-0.1.0.dist-info/top_level.txt,sha256=YxzuiHTGqHg_QHX9c0dchVO2jyu-gNFySLi324i9ZEo,34
29
+ tranquillity-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tranquillity = docker_launcher:main
@@ -0,0 +1,4 @@
1
+ ctOS
2
+ docker_launcher
3
+ settings
4
+ src