1235 lines
46 KiB
Python
1235 lines
46 KiB
Python
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
from datetime import datetime
|
||
|
||
_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
if _ROOT not in sys.path:
|
||
sys.path.insert(0, _ROOT)
|
||
|
||
from flask import Flask, render_template, request, jsonify
|
||
from flask_socketio import SocketIO, emit
|
||
|
||
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
|
||
|
||
|
||
def _first_enabled_user_index(config_path: str = "config/user.json") -> int:
|
||
try:
|
||
with open(config_path, "r", encoding="utf-8") as f:
|
||
users = json.load(f) or []
|
||
for i, u in enumerate(users):
|
||
if isinstance(u, dict) and u.get("enabled", True):
|
||
return i
|
||
except Exception:
|
||
pass
|
||
return 0
|
||
|
||
app = Flask(__name__)
|
||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||
|
||
@app.route('/')
|
||
def index():
|
||
return render_template('index.html')
|
||
|
||
@app.route('/api/flux', methods=['GET'])
|
||
def get_flux():
|
||
return jsonify(bot_controller.get_flux_list())
|
||
|
||
@app.route('/api/flux', methods=['POST'])
|
||
def add_flux():
|
||
data = request.json
|
||
channel_name = data.get('channel_name')
|
||
record_audio = data.get('record_audio', True)
|
||
|
||
if not channel_name:
|
||
return jsonify({'error': 'Nom du canal requis'}), 400
|
||
|
||
try:
|
||
flux_id = bot_controller.add_flux(channel_name, record_audio)
|
||
return jsonify({'success': True, 'flux_id': flux_id})
|
||
except Exception as e:
|
||
error_msg = f"Erreur lors de l'ajout du flux: {str(e)}"
|
||
print(f"Erreur API add_flux: {error_msg}")
|
||
return jsonify({'error': error_msg}), 500
|
||
|
||
@app.route('/api/flux/<int:flux_id>', methods=['DELETE'])
|
||
def remove_flux(flux_id):
|
||
if bot_controller.remove_flux(flux_id):
|
||
return jsonify({'success': True})
|
||
return jsonify({'error': 'Flux non trouvé'}), 404
|
||
|
||
@app.route('/api/flux/<int:flux_id>/status', methods=['GET'])
|
||
def get_flux_status(flux_id):
|
||
"""Obtenir le statut détaillé d'un flux spécifique"""
|
||
for flux in bot_controller.flux_list:
|
||
if flux['id'] == flux_id:
|
||
status = {
|
||
'id': flux_id,
|
||
'name': flux['name'],
|
||
'active': flux['active'],
|
||
'status': flux.get('status', 'unknown'),
|
||
'created_at': flux['created_at'],
|
||
'bots': {}
|
||
}
|
||
|
||
# Ajouter les informations des bots si disponibles
|
||
if flux_id in bot_controller.bots:
|
||
bots = bot_controller.bots[flux_id]
|
||
if bots['chat_bot']:
|
||
status['bots']['chat'] = {
|
||
'running': bots['chat_bot'].is_running if hasattr(bots['chat_bot'], 'is_running') else True
|
||
}
|
||
if bots['record_bot']:
|
||
status['bots']['record'] = {
|
||
'running': bots['record_bot'].running if hasattr(bots['record_bot'], 'running') else True
|
||
}
|
||
|
||
return jsonify(status)
|
||
|
||
return jsonify({'error': 'Flux non trouvé'}), 404
|
||
|
||
@app.route('/api/flux/<int:flux_id>/toggle', methods=['POST'])
|
||
def toggle_flux(flux_id):
|
||
"""Activer/désactiver un flux"""
|
||
for flux in bot_controller.flux_list:
|
||
if flux['id'] == flux_id:
|
||
flux['active'] = not flux['active']
|
||
|
||
# Arrêter/démarrer les bots selon le nouveau statut
|
||
if flux_id in bot_controller.bots:
|
||
bots = bot_controller.bots[flux_id]
|
||
if not flux['active']:
|
||
# Arrêter les bots
|
||
try:
|
||
if bots['chat_bot']:
|
||
bots['chat_bot'].stop()
|
||
if bots['record_bot']:
|
||
bots['record_bot'].stop()
|
||
if bots['subtitle_bot']:
|
||
bots['subtitle_bot'].stop()
|
||
if bots['ia_bot']:
|
||
bots['ia_bot'].stop()
|
||
if bots['message_bot']:
|
||
bots['message_bot'].stop()
|
||
except Exception as e:
|
||
print(f"Erreur lors de l'arrêt des bots: {e}")
|
||
else:
|
||
# Redémarrer les bots
|
||
try:
|
||
if bots['chat_bot']:
|
||
bots['chat_bot'].start_background()
|
||
if bots['record_bot']:
|
||
threading.Thread(target=bots['record_bot'].main, daemon=True).start()
|
||
if bots['subtitle_bot']:
|
||
bots['subtitle_bot'].start_main_loop()
|
||
if bots['ia_bot']:
|
||
bots['ia_bot'].start_main_loop()
|
||
if bots['message_bot']:
|
||
bots['message_bot'].start_loop_respond()
|
||
except Exception as e:
|
||
print(f"Erreur lors du redémarrage des bots: {e}")
|
||
|
||
return jsonify({'success': True, 'active': flux['active']})
|
||
|
||
return jsonify({'error': 'Flux non trouvé'}), 404
|
||
|
||
@app.route('/api/config/prompts', methods=['GET'])
|
||
def get_prompts():
|
||
return jsonify(bot_controller.config.get('list_prompt', []))
|
||
|
||
@app.route('/api/config/prompts', methods=['POST'])
|
||
def update_prompts():
|
||
data = request.json
|
||
prompts = data.get('prompts', [])
|
||
bot_controller.config['list_prompt'] = prompts
|
||
bot_controller.save_config()
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@app.route('/api/config/settings', methods=['GET'])
|
||
def get_settings():
|
||
"""Récupérer les paramètres généraux (config/config.json)."""
|
||
try:
|
||
cfg = bot_controller.load_config()
|
||
return jsonify({
|
||
'success': True,
|
||
'settings': {
|
||
'twitch_message_max_chars': int(cfg.get('twitch_message_max_chars', 100)),
|
||
}
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/settings', methods=['POST'])
|
||
def save_settings():
|
||
"""Sauvegarder les paramètres généraux (config/config.json)."""
|
||
try:
|
||
data = request.json or {}
|
||
settings = data.get('settings') or {}
|
||
|
||
cfg = bot_controller.load_config()
|
||
try:
|
||
cfg['twitch_message_max_chars'] = int(settings.get('twitch_message_max_chars', cfg.get('twitch_message_max_chars', 100)))
|
||
except Exception:
|
||
cfg['twitch_message_max_chars'] = 100
|
||
|
||
bot_controller.config = cfg
|
||
bot_controller.save_config()
|
||
return jsonify({'success': True, 'settings': {'twitch_message_max_chars': cfg['twitch_message_max_chars']}})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
@app.route('/api/config/users', methods=['GET'])
|
||
def get_users():
|
||
"""Récupérer la liste des utilisateurs"""
|
||
try:
|
||
with open('config/user.json', 'r') as file:
|
||
users = json.load(file)
|
||
return jsonify(users)
|
||
except FileNotFoundError:
|
||
return jsonify([])
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/config/users', methods=['POST'])
|
||
def add_user():
|
||
"""Ajouter un nouvel utilisateur"""
|
||
data = request.json
|
||
pseudo = data.get('tw_acc_pseudo')
|
||
token = data.get('tw_acc_token')
|
||
charactere = data.get('charactere', '😊')
|
||
enabled = bool(data.get('enabled', True))
|
||
interaction_bypass_antiloop = bool(data.get('interaction_bypass_antiloop', False))
|
||
|
||
if not pseudo or not token:
|
||
return jsonify({'error': 'Pseudo et token requis'}), 400
|
||
|
||
try:
|
||
with open('config/user.json', 'r') as file:
|
||
users = json.load(file)
|
||
|
||
# Vérifier si l'utilisateur existe déjà
|
||
for user in users:
|
||
if user['tw_acc_pseudo'] == pseudo:
|
||
return jsonify({'error': 'Cet utilisateur existe déjà'}), 400
|
||
|
||
# Ajouter le nouvel utilisateur
|
||
new_user = {
|
||
'tw_acc_pseudo': pseudo,
|
||
'tw_acc_token': token,
|
||
'charactere': charactere,
|
||
'enabled': enabled,
|
||
'interaction_bypass_antiloop': interaction_bypass_antiloop,
|
||
}
|
||
users.append(new_user)
|
||
|
||
# Sauvegarder
|
||
with open('config/user.json', 'w') as file:
|
||
json.dump(users, file, indent=4, ensure_ascii=False)
|
||
|
||
return jsonify({'success': True, 'user': new_user})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/config/users/<int:user_id>', methods=['PUT'])
|
||
def update_user(user_id):
|
||
"""Modifier un utilisateur existant"""
|
||
data = request.json
|
||
pseudo = data.get('tw_acc_pseudo')
|
||
token = data.get('tw_acc_token')
|
||
charactere = data.get('charactere', '😊')
|
||
enabled = bool(data.get('enabled', True))
|
||
interaction_bypass_antiloop = bool(data.get('interaction_bypass_antiloop', False))
|
||
|
||
if not pseudo or not token:
|
||
return jsonify({'error': 'Pseudo et token requis'}), 400
|
||
|
||
try:
|
||
with open('config/user.json', 'r') as file:
|
||
users = json.load(file)
|
||
|
||
if user_id >= len(users):
|
||
return jsonify({'error': 'Utilisateur non trouvé'}), 404
|
||
|
||
# Vérifier si le pseudo existe déjà (sauf pour l'utilisateur actuel)
|
||
for i, user in enumerate(users):
|
||
if i != user_id and user['tw_acc_pseudo'] == pseudo:
|
||
return jsonify({'error': 'Ce pseudo est déjà utilisé'}), 400
|
||
|
||
# Mettre à jour l'utilisateur
|
||
users[user_id] = {
|
||
'tw_acc_pseudo': pseudo,
|
||
'tw_acc_token': token,
|
||
'charactere': charactere,
|
||
'enabled': enabled,
|
||
'interaction_bypass_antiloop': interaction_bypass_antiloop,
|
||
}
|
||
|
||
# Sauvegarder
|
||
with open('config/user.json', 'w') as file:
|
||
json.dump(users, file, indent=4, ensure_ascii=False)
|
||
|
||
return jsonify({'success': True, 'user': users[user_id]})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/users/<int:user_id>/enabled', methods=['POST'])
|
||
def set_user_enabled(user_id):
|
||
"""Activer/désactiver un utilisateur sans toucher pseudo/token."""
|
||
try:
|
||
data = request.json or {}
|
||
enabled = bool(data.get('enabled', True))
|
||
|
||
with open('config/user.json', 'r', encoding='utf-8') as file:
|
||
users = json.load(file)
|
||
|
||
if user_id >= len(users):
|
||
return jsonify({'error': 'Utilisateur non trouvé'}), 404
|
||
|
||
if not isinstance(users[user_id], dict):
|
||
return jsonify({'error': 'Utilisateur invalide'}), 400
|
||
|
||
users[user_id]['enabled'] = enabled
|
||
|
||
with open('config/user.json', 'w', encoding='utf-8') as file:
|
||
json.dump(users, file, indent=4, ensure_ascii=False)
|
||
|
||
return jsonify({'success': True, 'user': users[user_id]})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/users/<int:user_id>/interaction-bypass', methods=['POST'])
|
||
def set_user_interaction_bypass(user_id):
|
||
"""Autoriser un utilisateur à bypass l’anti-boucle Interaction chat."""
|
||
try:
|
||
data = request.json or {}
|
||
bypass = bool(data.get('interaction_bypass_antiloop', False))
|
||
|
||
with open('config/user.json', 'r', encoding='utf-8') as file:
|
||
users = json.load(file)
|
||
|
||
if user_id >= len(users):
|
||
return jsonify({'error': 'Utilisateur non trouvé'}), 404
|
||
|
||
if not isinstance(users[user_id], dict):
|
||
return jsonify({'error': 'Utilisateur invalide'}), 400
|
||
|
||
users[user_id]['interaction_bypass_antiloop'] = bypass
|
||
|
||
with open('config/user.json', 'w', encoding='utf-8') as file:
|
||
json.dump(users, file, indent=4, ensure_ascii=False)
|
||
|
||
return jsonify({'success': True, 'user': users[user_id]})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/config/users/<int:user_id>', methods=['DELETE'])
|
||
def delete_user(user_id):
|
||
"""Supprimer un utilisateur"""
|
||
try:
|
||
with open('config/user.json', 'r') as file:
|
||
users = json.load(file)
|
||
|
||
if user_id >= len(users):
|
||
return jsonify({'error': 'Utilisateur non trouvé'}), 404
|
||
|
||
# Supprimer l'utilisateur
|
||
deleted_user = users.pop(user_id)
|
||
|
||
# Sauvegarder
|
||
with open('config/user.json', 'w') as file:
|
||
json.dump(users, file, indent=4, ensure_ascii=False)
|
||
|
||
return jsonify({'success': True, 'deleted_user': deleted_user})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/subtitles', methods=['GET'])
|
||
def get_subtitles():
|
||
data = storage.read("subtitle_data")
|
||
return jsonify(data)
|
||
|
||
@app.route('/api/subtitles/clear', methods=['POST'])
|
||
def clear_subtitles():
|
||
"""Nettoyer l'historique des sous-titres"""
|
||
try:
|
||
# Récupérer toutes les clés de sous-titres
|
||
subtitle_data = storage.read("subtitle_data")
|
||
|
||
# Supprimer chaque clé une par une
|
||
for key in list(subtitle_data.keys()):
|
||
storage.delete("subtitle_data", key)
|
||
|
||
# Supprimer également le fichier JSON s'il existe
|
||
storage_dir = "storage"
|
||
subtitle_file = os.path.join(storage_dir, "subtitle_data.json")
|
||
if os.path.exists(subtitle_file):
|
||
os.remove(subtitle_file)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Historique des sous-titres nettoyé avec succès'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors du nettoyage: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/subtitles/process', methods=['POST'])
|
||
def process_subtitles():
|
||
"""Lancer manuellement le traitement des sous-titres"""
|
||
try:
|
||
import subprocess
|
||
import os
|
||
import json
|
||
from datetime import datetime
|
||
|
||
# Vérifier que le dossier record existe
|
||
record_dir = "record"
|
||
if not os.path.exists(record_dir):
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'Dossier record non trouvé'
|
||
}), 404
|
||
|
||
# Trouver les fichiers audio
|
||
audio_files = [f for f in os.listdir(record_dir) if f.endswith('.mp3')]
|
||
if not audio_files:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'Aucun fichier audio trouvé'
|
||
}), 404
|
||
|
||
subtitles_created = 0
|
||
subtitles_data = {}
|
||
|
||
# Traiter chaque fichier audio
|
||
for audio_file in audio_files[:3]: # Limiter à 3 fichiers pour éviter de surcharger
|
||
audio_path = os.path.join(record_dir, audio_file)
|
||
|
||
try:
|
||
# Lancer Whisper
|
||
command = [
|
||
'whisper',
|
||
'--language', 'fr',
|
||
audio_path,
|
||
'--device', 'cuda',
|
||
'--model', 'large-v3'
|
||
]
|
||
|
||
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||
|
||
if result.returncode == 0:
|
||
# Lire le fichier .txt généré
|
||
txt_file = audio_file.replace('.mp3', '.txt')
|
||
if os.path.exists(txt_file):
|
||
with open(txt_file, 'r', encoding='utf-8') as f:
|
||
content = f.read().strip()
|
||
|
||
# Nettoyer le contenu
|
||
content = content.replace("'", "").replace('"', "").replace("\n", " ").replace(",", "")
|
||
|
||
# Supprimer les répétitions de mots
|
||
words = content.split()
|
||
seen = set()
|
||
result_words = []
|
||
for word in words:
|
||
if word not in seen:
|
||
result_words.append(word)
|
||
seen.add(word)
|
||
cleaned_content = " ".join(result_words)
|
||
|
||
# Sauvegarder dans le stockage
|
||
current_time = datetime.now().strftime('%H:%M:%S')
|
||
subtitles_data[current_time] = cleaned_content
|
||
|
||
# Sauvegarder dans le fichier de stockage
|
||
storage_dir = "storage"
|
||
if not os.path.exists(storage_dir):
|
||
os.makedirs(storage_dir)
|
||
|
||
subtitle_file = os.path.join(storage_dir, "subtitle_data.json")
|
||
try:
|
||
with open(subtitle_file, 'r', encoding='utf-8') as f:
|
||
existing_data = json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
existing_data = {}
|
||
|
||
existing_data[current_time] = cleaned_content
|
||
|
||
with open(subtitle_file, 'w', encoding='utf-8') as f:
|
||
json.dump(existing_data, f, indent=4, ensure_ascii=False)
|
||
|
||
subtitles_created += 1
|
||
|
||
# Nettoyer les fichiers temporaires
|
||
os.remove(txt_file)
|
||
os.remove(audio_path) # Supprimer le fichier audio traité
|
||
|
||
except subprocess.TimeoutExpired:
|
||
continue # Passer au fichier suivant
|
||
except Exception as e:
|
||
continue # Passer au fichier suivant
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'Traitement terminé. {subtitles_created} sous-titre(s) créé(s).',
|
||
'subtitles': subtitles_data
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors du traitement: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/generations', methods=['GET'])
|
||
def get_generations():
|
||
data = storage.read("IA_generator")
|
||
return jsonify(data)
|
||
|
||
@app.route('/api/send-message', methods=['POST'])
|
||
def send_message():
|
||
data = request.json
|
||
message = data.get('message')
|
||
channel = data.get('channel', 'default')
|
||
user_id = data.get('user_id', 0) # Nouveau paramètre pour choisir l'utilisateur
|
||
|
||
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)
|
||
# Utiliser send_message_user au lieu de send_message pour spécifier l'utilisateur
|
||
msg_bot.send_message_user(user_id, message)
|
||
return jsonify({'success': True})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/generate-response', methods=['POST'])
|
||
def generate_response():
|
||
data = request.json
|
||
text = data.get('text', '')
|
||
|
||
try:
|
||
ia_gen = IA_generator("config/config.json")
|
||
ia_gen.main_ask(text)
|
||
return jsonify({'success': True})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/chat/<int:flux_id>/messages', methods=['GET'])
|
||
def get_chat_messages(flux_id):
|
||
"""Récupérer les messages de chat d'un flux spécifique"""
|
||
try:
|
||
if flux_id not in bot_controller.bots:
|
||
return jsonify({'error': 'Flux non trouvé'}), 404
|
||
|
||
chat_bot = bot_controller.bots[flux_id]['chat_bot']
|
||
if not chat_bot:
|
||
return jsonify({'error': 'Bot de chat non disponible'}), 404
|
||
|
||
# Récupérer les messages du bot de chat
|
||
messages = []
|
||
for msg in chat_bot.messages[-50:]: # Derniers 50 messages
|
||
messages.append({
|
||
'timestamp': msg.timestamp.isoformat(),
|
||
'username': msg.username,
|
||
'content': msg.content
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'flux_id': flux_id,
|
||
'messages': messages
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/chat/<int:flux_id>/send', methods=['POST'])
|
||
def send_chat_message(flux_id):
|
||
"""Envoyer un message dans le chat d'un flux spécifique"""
|
||
data = request.json
|
||
message = data.get('message', '')
|
||
user_id = data.get('user_id', 0) # Nouveau paramètre pour choisir l'utilisateur
|
||
|
||
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
|
||
for f in bot_controller.flux_list:
|
||
if f['id'] == flux_id:
|
||
flux = f
|
||
break
|
||
|
||
if not flux:
|
||
return jsonify({'error': 'Flux non trouvé'}), 404
|
||
|
||
# Envoyer le message avec l'utilisateur spécifié
|
||
msg_bot = messageTwitch("config/user.json", flux['twitchname'])
|
||
msg_bot.send_message_user(user_id, message)
|
||
|
||
return jsonify({'success': True})
|
||
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
@app.route('/api/status', methods=['GET'])
|
||
def get_status():
|
||
status = {
|
||
'flux_count': len(bot_controller.flux_list),
|
||
'active_recordings': sum(1 for f in bot_controller.flux_list if f['record_audio'] and f['active']),
|
||
'chat_connections': sum(1 for f in bot_controller.flux_list if f['active']),
|
||
'last_subtitle': '',
|
||
'next_message': '',
|
||
'recent_messages': []
|
||
}
|
||
|
||
# Récupérer le dernier sous-titre
|
||
subtitle_data = storage.read("subtitle_data")
|
||
if subtitle_data:
|
||
sorted_keys = sorted(subtitle_data.keys())
|
||
if sorted_keys:
|
||
status['last_subtitle'] = subtitle_data[sorted_keys[-1]]
|
||
|
||
# Récupérer la dernière génération
|
||
generation_data = storage.read("IA_generator")
|
||
if generation_data:
|
||
sorted_keys = sorted(generation_data.keys())
|
||
if sorted_keys:
|
||
status['next_message'] = generation_data[sorted_keys[-1]]
|
||
|
||
return jsonify(status)
|
||
|
||
@socketio.on('connect')
|
||
def handle_connect():
|
||
print('Client connecté')
|
||
emit('status', {'message': 'Connecté au serveur'})
|
||
|
||
@socketio.on('disconnect')
|
||
def handle_disconnect():
|
||
print('Client déconnecté')
|
||
|
||
# Thread pour envoyer les mises à jour en temps réel
|
||
def background_updates():
|
||
while True:
|
||
try:
|
||
status = {
|
||
'timestamp': datetime.now().isoformat(),
|
||
'flux_count': len(bot_controller.flux_list),
|
||
'active_recordings': sum(1 for f in bot_controller.flux_list if f['record_audio'] and f['active']),
|
||
}
|
||
socketio.emit('status_update', status)
|
||
time.sleep(5) # Mise à jour toutes les 5 secondes
|
||
except Exception as e:
|
||
print(f"Erreur dans background_updates: {e}")
|
||
time.sleep(10)
|
||
|
||
# Variables globales pour la génération automatique
|
||
auto_subtitle_running = False
|
||
current_processing_file = None
|
||
|
||
# Variables globales pour l'envoi automatique de messages
|
||
auto_message_running = False
|
||
current_message_bot = None
|
||
|
||
@app.route('/api/subtitles/auto/start', methods=['POST'])
|
||
def start_auto_subtitle():
|
||
"""Démarrer la génération automatique de sous-titres"""
|
||
global auto_subtitle_running
|
||
try:
|
||
# Vérifier si déjà en cours
|
||
if auto_subtitle_running:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'La génération automatique est déjà en cours'
|
||
}), 400
|
||
|
||
auto_subtitle_running = True
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Démarrage de la génération automatique de sous-titres")
|
||
|
||
# Démarrer le thread de génération automatique
|
||
threading.Thread(target=auto_subtitle_loop, daemon=True).start()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Génération automatique démarrée'
|
||
})
|
||
except Exception as e:
|
||
auto_subtitle_running = False
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors du démarrage: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/subtitles/auto/stop', methods=['POST'])
|
||
def stop_auto_subtitle():
|
||
"""Arrêter la génération automatique de sous-titres"""
|
||
global auto_subtitle_running
|
||
try:
|
||
if not auto_subtitle_running:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'La génération automatique n\'est pas en cours'
|
||
}), 400
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt de la génération automatique de sous-titres")
|
||
auto_subtitle_running = False
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Génération automatique arrêtée'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors de l\'arrêt: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/subtitles/auto/force-stop', methods=['POST'])
|
||
def force_stop_auto_subtitle():
|
||
"""Forcer l'arrêt de la génération automatique de sous-titres"""
|
||
global auto_subtitle_running, current_processing_file
|
||
try:
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt forcé de la génération automatique de sous-titres")
|
||
auto_subtitle_running = False
|
||
current_processing_file = None
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Arrêt forcé de la génération automatique de sous-titres'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors de l\'arrêt forcé: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/subtitles/auto/status', methods=['GET'])
|
||
def get_auto_subtitle_status():
|
||
"""Obtenir le statut de la génération automatique"""
|
||
global auto_subtitle_running, current_processing_file
|
||
return jsonify({
|
||
'running': auto_subtitle_running,
|
||
'current_file': current_processing_file
|
||
})
|
||
|
||
def auto_subtitle_loop():
|
||
"""Boucle de génération automatique de sous-titres"""
|
||
global auto_subtitle_running, current_processing_file
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Démarrage de la boucle de génération automatique de sous-titres")
|
||
|
||
while auto_subtitle_running:
|
||
try:
|
||
# Vérifier s'il y a des fichiers audio à traiter
|
||
record_dir = "record"
|
||
if not os.path.exists(record_dir):
|
||
time.sleep(5)
|
||
continue
|
||
|
||
audio_files = [f for f in os.listdir(record_dir) if f.endswith('.mp3')]
|
||
if not audio_files:
|
||
time.sleep(5)
|
||
continue
|
||
|
||
# Traiter le premier fichier
|
||
audio_file = audio_files[0]
|
||
current_processing_file = audio_file
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Traitement de: {audio_file}")
|
||
|
||
# Émettre l'événement de début de traitement
|
||
socketio.emit('subtitle_processing_start', {
|
||
'file': audio_file,
|
||
'timestamp': datetime.now().isoformat()
|
||
})
|
||
|
||
# Traiter le fichier
|
||
audio_path = os.path.join(record_dir, audio_file)
|
||
|
||
try:
|
||
# Lancer Whisper avec vérification périodique de l'arrêt
|
||
command = [
|
||
'whisper',
|
||
'--language', 'fr',
|
||
audio_path,
|
||
'--device', 'cuda',
|
||
'--model', 'large-v3'
|
||
]
|
||
|
||
# Utiliser Popen pour pouvoir interrompre le processus
|
||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||
|
||
# Attendre la fin du processus avec vérification périodique
|
||
while process.poll() is None:
|
||
if not auto_subtitle_running:
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt demandé, interruption du processus Whisper")
|
||
process.terminate()
|
||
try:
|
||
process.wait(timeout=5)
|
||
except subprocess.TimeoutExpired:
|
||
process.kill()
|
||
break
|
||
time.sleep(1)
|
||
|
||
# Si la boucle a été arrêtée, sortir
|
||
if not auto_subtitle_running:
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt de la boucle de génération automatique")
|
||
break
|
||
|
||
result = process.returncode
|
||
stdout, stderr = process.communicate()
|
||
|
||
if result == 0:
|
||
# Lire le fichier .txt généré
|
||
txt_file = audio_file.replace('.mp3', '.txt')
|
||
if os.path.exists(txt_file):
|
||
with open(txt_file, 'r', encoding='utf-8') as f:
|
||
content = f.read().strip()
|
||
|
||
# Nettoyer le contenu
|
||
content = content.replace("'", "").replace('"', "").replace("\n", " ").replace(",", "")
|
||
|
||
# Supprimer les répétitions de mots
|
||
words = content.split()
|
||
seen = set()
|
||
result_words = []
|
||
for word in words:
|
||
if word not in seen:
|
||
result_words.append(word)
|
||
seen.add(word)
|
||
cleaned_content = " ".join(result_words)
|
||
|
||
# Sauvegarder dans le stockage
|
||
current_time = datetime.now().strftime('%H:%M:%S')
|
||
|
||
# Sauvegarder dans le fichier de stockage
|
||
storage_dir = "storage"
|
||
if not os.path.exists(storage_dir):
|
||
os.makedirs(storage_dir)
|
||
|
||
subtitle_file = os.path.join(storage_dir, "subtitle_data.json")
|
||
try:
|
||
with open(subtitle_file, 'r', encoding='utf-8') as f:
|
||
existing_data = json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
existing_data = {}
|
||
|
||
existing_data[current_time] = cleaned_content
|
||
|
||
with open(subtitle_file, 'w', encoding='utf-8') as f:
|
||
json.dump(existing_data, f, indent=4, ensure_ascii=False)
|
||
|
||
# Émettre l'événement de fin de traitement
|
||
socketio.emit('subtitle_processing_complete', {
|
||
'file': audio_file,
|
||
'subtitle': cleaned_content,
|
||
'timestamp': current_time
|
||
})
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Sous-titre généré: {cleaned_content}")
|
||
|
||
# Nettoyer les fichiers temporaires
|
||
os.remove(txt_file)
|
||
os.remove(audio_path)
|
||
|
||
else:
|
||
# Émettre l'événement d'erreur
|
||
socketio.emit('subtitle_processing_error', {
|
||
'file': audio_file,
|
||
'error': 'Aucun fichier .txt généré'
|
||
})
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Erreur: Aucun fichier .txt généré")
|
||
|
||
else:
|
||
# Émettre l'événement d'erreur
|
||
socketio.emit('subtitle_processing_error', {
|
||
'file': audio_file,
|
||
'error': f'Erreur Whisper: {stderr}'
|
||
})
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Erreur Whisper: {stderr}")
|
||
|
||
except subprocess.TimeoutExpired:
|
||
socketio.emit('subtitle_processing_error', {
|
||
'file': audio_file,
|
||
'error': 'Timeout - Whisper a pris trop de temps'
|
||
})
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Timeout - Whisper a pris trop de temps")
|
||
except Exception as e:
|
||
socketio.emit('subtitle_processing_error', {
|
||
'file': audio_file,
|
||
'error': f'Erreur: {str(e)}'
|
||
})
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Erreur lors du traitement: {str(e)}")
|
||
|
||
current_processing_file = None
|
||
|
||
# Vérifier à nouveau si la boucle doit continuer
|
||
if not auto_subtitle_running:
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt de la boucle de génération automatique")
|
||
break
|
||
|
||
# Attendre avant de traiter le prochain fichier
|
||
time.sleep(2)
|
||
|
||
except Exception as e:
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Erreur dans la boucle de génération automatique: {e}")
|
||
time.sleep(5)
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Fin de la boucle de génération automatique de sous-titres")
|
||
|
||
@app.route('/api/messages/auto/start', methods=['POST'])
|
||
def start_auto_message():
|
||
"""Démarrer l'envoi automatique de messages"""
|
||
global auto_message_running
|
||
try:
|
||
# Vérifier si déjà en cours
|
||
if auto_message_running:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'L\'envoi automatique est déjà en cours'
|
||
}), 400
|
||
|
||
auto_message_running = True
|
||
|
||
# Créer une instance du bot de messages
|
||
current_message_bot = messageTwitch("config/user.json", "default")
|
||
|
||
# Démarrer le thread d'envoi automatique
|
||
threading.Thread(target=auto_message_loop, daemon=True).start()
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Envoi automatique de messages démarré")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Envoi automatique de messages démarré'
|
||
})
|
||
except Exception as e:
|
||
auto_message_running = False
|
||
current_message_bot = None
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors du démarrage: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/messages/auto/stop', methods=['POST'])
|
||
def stop_auto_message():
|
||
"""Arrêter l'envoi automatique de messages"""
|
||
global auto_message_running, current_message_bot
|
||
try:
|
||
if not auto_message_running:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'L\'envoi automatique n\'est pas en cours'
|
||
}), 400
|
||
|
||
auto_message_running = False
|
||
current_message_bot = None
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Envoi automatique de messages arrêté")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Envoi automatique de messages arrêté'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors de l\'arrêt: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/messages/auto/force-stop', methods=['POST'])
|
||
def force_stop_auto_message():
|
||
"""Forcer l'arrêt de l'envoi automatique de messages"""
|
||
global auto_message_running, current_message_bot
|
||
try:
|
||
auto_message_running = False
|
||
current_message_bot = None
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt forcé de l'envoi automatique de messages")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Arrêt forcé de l\'envoi automatique de messages'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors de l\'arrêt forcé: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/messages/auto/status', methods=['GET'])
|
||
def get_auto_message_status():
|
||
"""Obtenir le statut de l'envoi automatique de messages"""
|
||
global auto_message_running
|
||
return jsonify({
|
||
'running': auto_message_running
|
||
})
|
||
|
||
def auto_message_loop():
|
||
"""Boucle d'envoi automatique de messages"""
|
||
global auto_message_running, current_message_bot
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Démarrage de la boucle d'envoi automatique")
|
||
|
||
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:
|
||
time.sleep(5)
|
||
continue
|
||
|
||
# Récupérer la dernière génération
|
||
sorted_keys = sorted(generation_data.keys())
|
||
if not sorted_keys:
|
||
time.sleep(5)
|
||
continue
|
||
|
||
last_generation = generation_data[sorted_keys[-1]]
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Envoi automatique: {last_generation}")
|
||
|
||
# Émettre l'événement de début d'envoi
|
||
socketio.emit('message_sending_start', {
|
||
'message': last_generation,
|
||
'timestamp': datetime.now().isoformat()
|
||
})
|
||
|
||
# Envoyer le message
|
||
try:
|
||
if current_message_bot:
|
||
# Utiliser le premier utilisateur par défaut
|
||
current_message_bot.send_message_user(_first_enabled_user_index(), last_generation)
|
||
|
||
# Émettre l'événement de fin d'envoi
|
||
socketio.emit('message_sending_complete', {
|
||
'message': last_generation,
|
||
'timestamp': datetime.now().isoformat()
|
||
})
|
||
|
||
# Supprimer la génération envoyée
|
||
storage.delete("IA_generator", sorted_keys[-1])
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Message envoyé avec succès")
|
||
|
||
except Exception as e:
|
||
# Émettre l'événement d'erreur
|
||
socketio.emit('message_sending_error', {
|
||
'message': last_generation,
|
||
'error': str(e)
|
||
})
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Erreur d'envoi: {e}")
|
||
|
||
# Attendre avant d'envoyer le prochain message
|
||
time.sleep(10)
|
||
|
||
except Exception as e:
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Erreur dans la boucle d'envoi automatique: {e}")
|
||
time.sleep(5)
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Arrêt de la boucle d'envoi automatique")
|
||
|
||
@app.route('/api/chat/messages/enable', methods=['POST'])
|
||
def enable_chat_messages():
|
||
"""Activer l'envoi de messages dans le chat"""
|
||
try:
|
||
chat_state.chat_messages_enabled = True
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Envoi de messages dans le chat activé'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors de l\'activation: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/chat/messages/disable', methods=['POST'])
|
||
def disable_chat_messages():
|
||
"""Désactiver l'envoi de messages dans le chat"""
|
||
try:
|
||
chat_state.chat_messages_enabled = False
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Envoi de messages dans le chat désactivé'
|
||
})
|
||
except Exception as e:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Erreur lors de la désactivation: {str(e)}'
|
||
}), 500
|
||
|
||
@app.route('/api/chat/messages/status', methods=['GET'])
|
||
def get_chat_messages_status():
|
||
"""Obtenir le statut de l'envoi de messages dans le chat"""
|
||
return jsonify({
|
||
'enabled': chat_state.chat_messages_enabled
|
||
})
|
||
|
||
@app.route('/api/system-status', methods=['GET'])
|
||
def get_system_status():
|
||
"""Obtenir le statut de tous les composants"""
|
||
return jsonify(bot_controller.get_system_status())
|
||
|
||
@app.route('/api/interaction/config', methods=['GET'])
|
||
def get_interaction_config():
|
||
"""
|
||
Récupérer la config Interaction Chat (réponses préenregistrées + règles).
|
||
Stockée en JSON sous storage/interaction_chat_config.json.
|
||
"""
|
||
try:
|
||
# Utiliser le processor du premier flux actif si possible, sinon un "fallback" via une instance ad-hoc
|
||
processor = None
|
||
for flux_id, bots in bot_controller.bots.items():
|
||
processor = bots.get('interaction')
|
||
if processor:
|
||
break
|
||
|
||
if not processor:
|
||
# fallback: config globale, même fichier storage
|
||
from twitch_bot.interaction_chat import InteractionChatStorage
|
||
st = InteractionChatStorage()
|
||
cfg = InteractionChatConfig(st.read_json("interaction_chat_config", default={}))
|
||
data = cfg.to_dict()
|
||
else:
|
||
data = processor.load_config().to_dict()
|
||
|
||
# Comptes enregistrés (config/user.json)
|
||
try:
|
||
with open('config/user.json', 'r', encoding='utf-8') as f:
|
||
users = json.load(f)
|
||
registered = [u.get('tw_acc_pseudo') for u in (users or []) if isinstance(u, dict) and u.get('tw_acc_pseudo')]
|
||
except Exception:
|
||
registered = []
|
||
|
||
return jsonify({'success': True, 'config': data, 'registered_accounts': registered})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
@app.route('/api/interaction/config', methods=['POST'])
|
||
def save_interaction_config():
|
||
"""Sauvegarder la config Interaction Chat."""
|
||
try:
|
||
payload = request.json or {}
|
||
config_dict = payload.get('config') or {}
|
||
|
||
# Sauvegarder via un processor si présent, sinon via stockage direct
|
||
processor = None
|
||
for flux_id, bots in bot_controller.bots.items():
|
||
processor = bots.get('interaction')
|
||
if processor:
|
||
break
|
||
|
||
if processor:
|
||
cfg = processor.save_config(config_dict)
|
||
else:
|
||
from twitch_bot.interaction_chat import InteractionChatStorage
|
||
st = InteractionChatStorage()
|
||
cfg = InteractionChatConfig(config_dict)
|
||
st.write_json("interaction_chat_config", cfg.to_dict())
|
||
|
||
return jsonify({'success': True, 'config': cfg.to_dict()})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
@app.route('/api/interaction/log', methods=['GET'])
|
||
def get_interaction_log():
|
||
"""Lire les logs Interaction Chat."""
|
||
try:
|
||
limit = int(request.args.get('limit', '100'))
|
||
limit = max(1, min(500, limit))
|
||
|
||
processor = None
|
||
for flux_id, bots in bot_controller.bots.items():
|
||
processor = bots.get('interaction')
|
||
if processor:
|
||
break
|
||
|
||
if processor:
|
||
logs = processor.read_log(limit=limit)
|
||
else:
|
||
from twitch_bot.interaction_chat import InteractionChatStorage
|
||
st = InteractionChatStorage()
|
||
logs = st.read_json("interaction_chat_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/ia-generator/start', methods=['POST'])
|
||
def start_ia_generator():
|
||
"""Démarrer le générateur IA"""
|
||
success, message = bot_controller.start_ia_generator()
|
||
if success:
|
||
# Synchroniser l'état de l'interface web
|
||
socketio.emit('ia_generator_status_changed', {
|
||
'running': True,
|
||
'message': message
|
||
})
|
||
return jsonify({'success': success, 'message': message})
|
||
|
||
@app.route('/api/ia-generator/stop', methods=['POST'])
|
||
def stop_ia_generator():
|
||
"""Arrêter le générateur IA"""
|
||
success, message = bot_controller.stop_ia_generator()
|
||
if success:
|
||
# Synchroniser l'état de l'interface web
|
||
socketio.emit('ia_generator_status_changed', {
|
||
'running': False,
|
||
'message': message
|
||
})
|
||
return jsonify({'success': success, 'message': message})
|
||
|
||
@app.route('/api/control-twitch/start', methods=['POST'])
|
||
def start_control_twitch():
|
||
"""Démarrer le contrôleur Twitch"""
|
||
success, message = bot_controller.start_control_twitch()
|
||
return jsonify({'success': success, 'message': message})
|
||
|
||
@app.route('/api/control-twitch/stop', methods=['POST'])
|
||
def stop_control_twitch():
|
||
"""Arrêter le contrôleur Twitch"""
|
||
success, message = bot_controller.stop_control_twitch()
|
||
return jsonify({'success': success, 'message': message})
|
||
|
||
if __name__ == '__main__':
|
||
# Démarrer les mises à jour en arrière-plan
|
||
update_thread = threading.Thread(target=background_updates, daemon=True)
|
||
update_thread.start()
|
||
|
||
# Démarrer l'application Flask
|
||
socketio.run(app, host='0.0.0.0', port=5000, debug=True) |