update update

This commit is contained in:
gpatruno
2026-04-28 21:06:26 +02:00
parent 7b2135bfed
commit b4254c9e06
28 changed files with 2032 additions and 547 deletions
+334 -23
View File
@@ -17,6 +17,7 @@ from fonction.first_class import IA_generator, messageTwitch, storage
from twitch_bot import chat_state
from twitch_bot.controller import bot_controller
from twitch_bot.interaction_chat import InteractionChatConfig
import requests
def _first_enabled_user_index(config_path: str = "config/user.json") -> int:
@@ -30,6 +31,114 @@ def _first_enabled_user_index(config_path: str = "config/user.json") -> int:
pass
return 0
def _read_json_file(path: str, default):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if data is not None else default
except Exception:
return default
def _twitch_client_id() -> str:
cfg = _read_json_file("config/config.json", default={})
if isinstance(cfg, dict):
v = cfg.get("twitch_client_id") or cfg.get("twitch_clientId") or cfg.get("twitchClientId")
if isinstance(v, str):
return v.strip()
return ""
def _user_bearer_token(user_id: int) -> str:
users = _read_json_file("config/user.json", default=[])
if not isinstance(users, list) or user_id < 0 or user_id >= len(users):
raise ValueError("Utilisateur non trouvé")
u = users[user_id]
if not isinstance(u, dict):
raise ValueError("Utilisateur invalide")
raw = (u.get("tw_acc_token") or "").strip()
if raw.lower().startswith("oauth:"):
raw = raw.split(":", 1)[1].strip()
if not raw:
raise ValueError("Token OAuth manquant")
return raw
def _twitch_helix_headers(user_bearer: str) -> dict:
client_id = _twitch_client_id()
if not client_id:
raise ValueError("Client-ID Twitch manquant. Ajoutez `twitch_client_id` dans config/config.json")
return {
"Client-ID": client_id,
"Authorization": f"Bearer {user_bearer}",
}
def _twitch_get_user_id_by_login(login: str, user_bearer: str) -> str:
login = (login or "").strip().lstrip("@")
if not login:
raise ValueError("Nom de chaîne requis")
headers = _twitch_helix_headers(user_bearer)
r = requests.get("https://api.twitch.tv/helix/users", headers=headers, params={"login": login}, timeout=10)
try:
payload = r.json()
except Exception:
payload = None
if r.status_code >= 400:
msg = ""
if isinstance(payload, dict):
msg = payload.get("message") or payload.get("error") or ""
raise RuntimeError(f"Erreur Twitch /helix/users ({r.status_code}) {msg}".strip())
data = (payload or {}).get("data") if isinstance(payload, dict) else None
if not isinstance(data, list) or not data:
raise ValueError("Chaîne Twitch introuvable (login)")
user = data[0] or {}
broadcaster_id = user.get("id")
if not broadcaster_id:
raise RuntimeError("Réponse Twitch invalide: id manquant")
return str(broadcaster_id)
def _twitch_create_clip(broadcaster_id: str, user_bearer: str, has_delay: bool = False) -> dict:
headers = _twitch_helix_headers(user_bearer)
params = {"broadcaster_id": str(broadcaster_id), "has_delay": "true" if has_delay else "false"}
r = requests.post("https://api.twitch.tv/helix/clips", headers=headers, params=params, timeout=15)
try:
payload = r.json()
except Exception:
payload = None
if r.status_code >= 400:
msg = ""
if isinstance(payload, dict):
msg = payload.get("message") or payload.get("error") or ""
hint = ""
if r.status_code in (401, 403):
hint = " (vérifiez que le token a le scope `clips:edit` et qu'il n'est pas expiré)"
raise RuntimeError(f"Erreur Twitch /helix/clips ({r.status_code}) {msg}{hint}".strip())
data = (payload or {}).get("data") if isinstance(payload, dict) else None
if not isinstance(data, list) or not data:
raise RuntimeError("Réponse Twitch invalide: data manquant")
clip = data[0] or {}
clip_id = clip.get("id")
edit_url = clip.get("edit_url")
if not clip_id:
raise RuntimeError("Réponse Twitch invalide: clip id manquant")
# URL publique approximative (le retour officiel donne surtout edit_url)
public_url = f"https://clips.twitch.tv/{clip_id}"
return {
"id": clip_id,
"edit_url": edit_url,
"url": public_url,
}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
socketio = SocketIO(app, cors_allowed_origins="*")
@@ -47,12 +156,14 @@ def add_flux():
data = request.json
channel_name = data.get('channel_name')
record_audio = data.get('record_audio', True)
send_messages = bool(data.get('send_messages', True))
enable_ia = bool(data.get('enable_ia', True))
if not channel_name:
return jsonify({'error': 'Nom du canal requis'}), 400
try:
flux_id = bot_controller.add_flux(channel_name, record_audio)
flux_id = bot_controller.add_flux(channel_name, record_audio, send_messages, enable_ia)
return jsonify({'success': True, 'flux_id': flux_id})
except Exception as e:
error_msg = f"Erreur lors de l'ajout du flux: {str(e)}"
@@ -129,9 +240,10 @@ def toggle_flux(flux_id):
threading.Thread(target=bots['record_bot'].main, daemon=True).start()
if bots['subtitle_bot']:
bots['subtitle_bot'].start_main_loop()
if bots['ia_bot']:
if bool(flux.get("enable_ia", True)) and bots.get('ia_bot'):
bots['ia_bot'].start_main_loop()
if bots['message_bot']:
# Respecter le flag par-flux send_messages
if bool(flux.get("send_messages", True)) and bool(flux.get("enable_ia", True)) and bots.get('message_bot'):
bots['message_bot'].start_loop_respond()
except Exception as e:
print(f"Erreur lors du redémarrage des bots: {e}")
@@ -356,23 +468,99 @@ def delete_user(user_id):
@app.route('/api/subtitles', methods=['GET'])
def get_subtitles():
data = storage.read("subtitle_data")
return jsonify(data)
flux_id = request.args.get("flux_id")
channel = request.args.get("channel")
key = "subtitle_data"
if channel:
key = f"subtitle_data__{str(channel).strip().lower()}"
elif flux_id:
try:
fid = int(flux_id)
flux = next((f for f in bot_controller.flux_list if f.get("id") == fid), None)
if flux and flux.get("twitchname"):
key = f"subtitle_data__{str(flux.get('twitchname')).strip().lower()}"
except Exception:
pass
data = storage.read(key)
return jsonify(data if isinstance(data, dict) else {})
@app.route('/api/subtitles/last', methods=['GET'])
def get_last_subtitle():
"""Retourne le dernier sous-titre (clé + texte)."""
try:
flux_id = request.args.get("flux_id")
channel = request.args.get("channel")
key = "subtitle_data"
if channel:
key = f"subtitle_data__{str(channel).strip().lower()}"
elif flux_id:
try:
fid = int(flux_id)
flux = next((f for f in bot_controller.flux_list if f.get("id") == fid), None)
if flux and flux.get("twitchname"):
key = f"subtitle_data__{str(flux.get('twitchname')).strip().lower()}"
except Exception:
pass
subtitle_data = storage.read(key) or {}
if not isinstance(subtitle_data, dict) or not subtitle_data:
return jsonify({'success': True, 'last_key': None, 'last_subtitle': ''})
sorted_keys = sorted(subtitle_data.keys())
def _good_candidate(text: str) -> bool:
t = (text or "").strip()
if not t:
return False
# Heuristique: éviter les "bruits" très courts pour la génération IA
words = [w for w in t.split() if w]
return (len(t) >= 35) or (len(words) >= 8)
# Chercher le plus récent "utile", sinon fallback sur le tout dernier
picked_key = None
picked_text = ""
for k in reversed(sorted_keys):
t = subtitle_data.get(k, "")
if _good_candidate(str(t)):
picked_key = k
picked_text = str(t)
break
if picked_key is None:
picked_key = sorted_keys[-1]
picked_text = str(subtitle_data.get(picked_key, "") or "")
return jsonify({'success': True, 'last_key': picked_key, 'last_subtitle': picked_text})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/subtitles/clear', methods=['POST'])
def clear_subtitles():
"""Nettoyer l'historique des sous-titres"""
try:
flux_id = request.args.get("flux_id")
channel = request.args.get("channel")
storage_key = "subtitle_data"
if channel:
storage_key = f"subtitle_data__{str(channel).strip().lower()}"
elif flux_id:
try:
fid = int(flux_id)
flux = next((f for f in bot_controller.flux_list if f.get("id") == fid), None)
if flux and flux.get("twitchname"):
storage_key = f"subtitle_data__{str(flux.get('twitchname')).strip().lower()}"
except Exception:
pass
# Récupérer toutes les clés de sous-titres
subtitle_data = storage.read("subtitle_data")
subtitle_data = storage.read(storage_key)
# Supprimer chaque clé une par une
for key in list(subtitle_data.keys()):
storage.delete("subtitle_data", key)
for loop_key in list((subtitle_data or {}).keys()):
storage.delete(storage_key, loop_key)
# Supprimer également le fichier JSON s'il existe
storage_dir = "storage"
subtitle_file = os.path.join(storage_dir, "subtitle_data.json")
subtitle_file = os.path.join(storage_dir, f"{storage_key}.json")
if os.path.exists(subtitle_file):
os.remove(subtitle_file)
@@ -386,6 +574,74 @@ def clear_subtitles():
'error': f'Erreur lors du nettoyage: {str(e)}'
}), 500
@app.route('/api/subtitles/rules/config', methods=['GET'])
def get_subtitle_rules_config():
try:
processor = None
for flux_id, bots in bot_controller.bots.items():
processor = bots.get('subtitle_rules')
if processor:
break
if not processor:
from twitch_bot.subtitle_rules import SubtitleRulesStorage, SubtitleRulesConfig
st = SubtitleRulesStorage()
cfg = SubtitleRulesConfig(st.read_json("subtitle_rules_config", default={}))
data = cfg.to_dict()
else:
data = processor.load_config().to_dict()
return jsonify({'success': True, 'config': data})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/subtitles/rules/config', methods=['POST'])
def save_subtitle_rules_config():
try:
payload = request.json or {}
config_dict = payload.get('config') or {}
processor = None
for flux_id, bots in bot_controller.bots.items():
processor = bots.get('subtitle_rules')
if processor:
break
if processor:
cfg = processor.save_config(config_dict)
data = cfg.to_dict()
else:
from twitch_bot.subtitle_rules import SubtitleRulesStorage, SubtitleRulesConfig
st = SubtitleRulesStorage()
cfg = SubtitleRulesConfig(config_dict)
st.write_json("subtitle_rules_config", cfg.to_dict())
data = cfg.to_dict()
return jsonify({'success': True, 'config': data})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/subtitles/rules/log', methods=['GET'])
def get_subtitle_rules_log():
try:
limit = int(request.args.get('limit', '120'))
limit = max(1, min(500, limit))
processor = None
for flux_id, bots in bot_controller.bots.items():
processor = bots.get('subtitle_rules')
if processor:
break
if processor:
logs = processor.read_log(limit=limit)
else:
from twitch_bot.subtitle_rules import SubtitleRulesStorage
st = SubtitleRulesStorage()
logs = st.read_json("subtitle_rules_log", default=[])
if not isinstance(logs, list):
logs = []
logs = logs[-limit:]
return jsonify({'success': True, 'logs': logs})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/subtitles/process', methods=['POST'])
def process_subtitles():
"""Lancer manuellement le traitement des sous-titres"""
@@ -509,10 +765,6 @@ def send_message():
if not message:
return jsonify({'error': 'Message requis'}), 400
# Vérifier si l'envoi de messages est activé
if not chat_state.chat_messages_enabled:
return jsonify({'error': 'Envoi de messages désactivé'}), 403
# Trouver le bot de message pour ce canal
try:
msg_bot = messageTwitch("config/user.json", channel)
@@ -573,10 +825,6 @@ def send_chat_message(flux_id):
if not message:
return jsonify({'error': 'Message requis'}), 400
# Vérifier si l'envoi de messages est activé
if not chat_state.chat_messages_enabled:
return jsonify({'error': 'Envoi de messages désactivé'}), 403
try:
# Trouver le flux
flux = None
@@ -587,6 +835,9 @@ def send_chat_message(flux_id):
if not flux:
return jsonify({'error': 'Flux non trouvé'}), 404
if not bool(flux.get("send_messages", True)):
return jsonify({'error': 'Envoi de messages désactivé pour ce flux'}), 403
# Envoyer le message avec l'utilisateur spécifié
msg_bot = messageTwitch("config/user.json", flux['twitchname'])
@@ -998,12 +1249,6 @@ def auto_message_loop():
while auto_message_running:
try:
# Vérifier si l'envoi de messages est activé
if not chat_state.chat_messages_enabled:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Envoi de messages désactivé, attente...")
time.sleep(5)
continue
# Vérifier s'il y a des générations disponibles
generation_data = storage.read("IA_generator")
if not generation_data:
@@ -1102,6 +1347,72 @@ def get_system_status():
"""Obtenir le statut de tous les composants"""
return jsonify(bot_controller.get_system_status())
@app.route('/api/clips/create', methods=['POST'])
def create_clip():
"""
Créer un clip Twitch via Helix.
Nécessite:
- `twitch_client_id` dans config/config.json
- un user token avec scope `clips:edit` (config/user.json, champ tw_acc_token)
"""
try:
data = request.json or {}
channel_login = (data.get("channel_login") or "").strip()
user_id = int(data.get("user_id", _first_enabled_user_index()))
has_delay = bool(data.get("has_delay", False))
bearer = _user_bearer_token(user_id)
broadcaster_id = _twitch_get_user_id_by_login(channel_login, bearer)
clip = _twitch_create_clip(broadcaster_id=broadcaster_id, user_bearer=bearer, has_delay=has_delay)
return jsonify({
"success": True,
"clip": {
"broadcaster_login": channel_login.lstrip("@"),
"broadcaster_id": broadcaster_id,
**clip,
}
})
except ValueError as e:
return jsonify({"success": False, "error": str(e)}), 400
except RuntimeError as e:
return jsonify({"success": False, "error": str(e)}), 502
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# === CLIPS AUTOMATION (stub for future) ===
_clips_automation_running = False
_clips_automation_mode = "manual"
@app.route('/api/clips/automation/status', methods=['GET'])
def clip_automation_status():
return jsonify({
"success": True,
"running": _clips_automation_running,
"mode": _clips_automation_mode,
})
@app.route('/api/clips/automation/start', methods=['POST'])
def clip_automation_start():
# Intentionally not implemented yet: future hook point.
return jsonify({
"success": False,
"error": "Automatisation clips: pas encore implémentée (stub prêt)",
}), 501
@app.route('/api/clips/automation/stop', methods=['POST'])
def clip_automation_stop():
# Intentionally not implemented yet: future hook point.
return jsonify({
"success": False,
"error": "Automatisation clips: pas encore implémentée (stub prêt)",
}), 501
@app.route('/api/interaction/config', methods=['GET'])
def get_interaction_config():
"""