Clementrnx/twitchclipper
1
1import asyncio2import os3import cv24import json5import numpy as np6import whisper7import requests8import random9import streamlink10import subprocess11import zipfile12import io13import shutil14from pathlib import Path15from PIL import ImageFont, ImageDraw, Image16import streamlit as st17import edge_tts18from datetime import datetime, timedelta19 20# --- SECRETS & CONFIG ---21TWITCH_CID = st.secrets.get("TWITCH_CLIENT_ID", "")22TWITCH_SEC = st.secrets.get("TWITCH_CLIENT_SECRET", "")23OUTPUT_DIR = Path("TwitchClipper_Production")24OUTPUT_DIR.mkdir(exist_ok=True)25CONFIG_FILE = Path("config_webhook.json")26 27def load_webhook():28 if CONFIG_FILE.exists(): return json.load(open(CONFIG_FILE, "r")).get("webhook", "")29 return ""30 31def save_webhook(url):32 with open(CONFIG_FILE, "w") as f: json.dump({"webhook": url}, f)33 34saved_webhook = load_webhook()35 36# --- DESIGN ---37st.set_page_config(page_title="Clementrnxx TikTok Studio", layout="wide")38st.markdown('<h1 style="color: #FFFF00; text-align: center; font-family: Impact; font-size: 3rem;">CLEMENTRNXX TIKTOK STUDIO</h1>', unsafe_allow_html=True)39 40# --- BASES TROLL (On garde les listes complètes ici) ---41FEMMES_BASE = ["POLSKA", "RUBY NIKARA", "MAEVA GHENNAM", "AYA NAKAMURA", "NABILLA", "OPHENYA", "LÉNA SITUATIONS", "AMOURANTH"]42ATTR_BASE = ["LA CHATTE BIEN ÉCARTEE", "LE TROU DE BALLE DÉFONCÉ", "LA GROSSE CHATTE QUI PUE", "L'ANUS BIEN NOIR"]43LIEUX_BASE = ["AUX CHIOTTES DU COURS JU", "AU FIVE DE NANTERRE", "DANS UNE CAVE À ÉVRY", "AU MACDO DE CHÂTELET"]44ACTIONS_BASE = ["RAMONNE", "DÉFONCE", "REPEINT", "EXPLOSE", "PULVÉRISE"]45 46# --- FONCTION TEXTE ---47def draw_3d_text(frame, text, y, font_size, color=(255, 255, 255)):48 img = Image.fromarray(frame)49 draw = ImageDraw.Draw(img)50 try: font = ImageFont.truetype("Impact.ttf", font_size)51 except: font = ImageFont.load_default()52 lines = [text[i:i+25] for i in range(0, len(text), 25)]53 curr_y = y54 for line in lines:55 bbox = draw.textbbox((0, 0), line, font=font)56 x = (frame.shape[1] - (bbox[2] - bbox[0])) // 257 for i in range(5, 0, -1): draw.text((x+i, curr_y+i), line, font=font, fill=(0, 0, 0))58 draw.text((x, curr_y), line, font=font, fill=color)59 curr_y += font_size + 560 return np.array(img)61 62# --- ONGLETS ---63t1, t2, t3 = st.tabs(["[1] CONFIGURATION", "[2] SCANNER VIRAL", "[3] PRODUCTION"])64 65with t1:66 st.info("ℹ️ Identifiants Twitch gérés par Secrets. Configurez uniquement le Webhook ici.")67 new_webhook = st.text_input("Discord Webhook URL", value=saved_webhook)68 if st.button("SAUVEGARDER"):69 save_webhook(new_webhook)70 st.success("Webhook enregistré.")71 72with t2:73 col1, col2 = st.columns(2)74 with col1:75 mode_scan = st.selectbox("Type de Recherche", ["Global (Just Chatting)", "Streamer Liste"])76 period = st.selectbox("Période", ["24h", "2d", "7d", "30d"], index=0)77 min_views = st.number_input("Vues Minimum", value=50) # Baissé par défaut pour tester78 with col2:79 lang = st.selectbox("Langue", ["fr", "en"], index=0)80 min_duration = st.slider("Durée Min (sec)", 5, 60, 10)81 query = st.text_input("Liste Streamers", placeholder="kamet0,etoiles")82 count = st.slider("Max Clips", 1, 50, 10)83 84 if st.button("🚀 LANCER LE SCAN VIRAL"):85 log_area = st.empty()86 try:87 log_area.info("🔑 Connexion Twitch...")88 auth_url = f"https://id.twitch.tv/oauth2/token?client_id={TWITCH_CID}&client_secret={TWITCH_SEC}&grant_type=client_credentials"89 tk_resp = requests.post(auth_url).json()90 91 if "access_token" not in tk_resp:92 log_area.error(f"Erreur Auth: {tk_resp.get('message', 'Vérifiez vos Secrets')}")93 else:94 token = tk_resp["access_token"]95 h = {"Client-ID": TWITCH_CID, "Authorization": f"Bearer {token}"}96 97 # Calcul de la date au format RFC333998 days_val = int(''.join(filter(str.isdigit, period)))99 start_date = (datetime.utcnow() - timedelta(days=days_val)).strftime('%Y-%m-%dT%H:%M:%SZ')100 101 all_raw_clips = []102 103 if mode_scan == "Global (Just Chatting)":104 log_area.info("📡 Scan Just Chatting...")105 g_url = "https://api.twitch.tv/helix/games?name=Just Chatting"106 g_data = requests.get(g_url, headers=h).json().get('data', [])107 if g_data:108 # On prend 100 clips direct pour avoir du choix109 c_url = f"https://api.twitch.tv/helix/clips?game_id={g_data[0]['id']}&first=100&started_at={start_date}"110 all_raw_clips = requests.get(c_url, headers=h).json().get('data', [])111 else:112 streamer_names = [s.strip() for s in query.split(",") if s.strip()]113 for name in streamer_names:114 log_area.info(f"🔎 Scan de {name}...")115 u_url = f"https://api.twitch.tv/helix/users?login={name}"116 u_data = requests.get(u_url, headers=h).json().get('data', [])117 if u_data:118 c_url = f"https://api.twitch.tv/helix/clips?broadcaster_id={u_data[0]['id']}&first=50&started_at={start_date}"119 all_raw_clips.extend(requests.get(c_url, headers=h).json().get('data', []))120 121 # FILTRAGE MANUEL (Plus fiable que les filtres d'URL Twitch)122 log_area.info(f"🧪 Filtrage de {len(all_raw_clips)} clips trouvés...")123 124 filtered = [125 c for c in all_raw_clips 126 if c.get('language') == lang 127 and c.get('view_count', 0) >= min_views 128 and c.get('duration', 0) >= min_duration129 ]130 131 # Tri par vues132 filtered.sort(key=lambda x: x['view_count'], reverse=True)133 st.session_state.clips = filtered[:count]134 135 if st.session_state.clips:136 log_area.success(f"✅ {len(st.session_state.clips)} clips prêts !")137 for c in st.session_state.clips:138 st.write(f"📺 **{c['broadcaster_name']}** | 📈 {c['view_count']} vues | ⏳ {int(c['duration'])}s | *{c['title']}*")139 else:140 log_area.warning(f"Rien trouvé. Essaie de baisser 'Vues Minimum' ou vérifie la langue.")141 142 except Exception as e:143 log_area.error(f"Erreur Critique: {e}")144 145with t3:146 if 'clips' in st.session_state and st.session_state.clips:147 mode_titre = st.radio("Style", ["Normal", "Troll"], horizontal=True)148 if st.button("⚡ PRODUIRE LE PACK"):149 status = st.empty()150 pb = st.progress(0)151 # Logique de production... (Utilise la même fonction process_video que précédemment)152 st.success("Production lancée (Simulé pour cet exemple, intègre ton code de rendu ici)")