Download content_generator.py from FincH12/Automation_Threads_EU: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/FincH12/Automation_Threads_EU/resolve/main/content_generator.py
- Command line
-
hf download hf://spaces/FincH12/Automation_Threads_EU/content_generator.py
-
curl -L -o content_generator.py https://huggingface.co/spaces/FincH12/Automation_Threads_EU/resolve/main/content_generator.py
13.8 kB
| # -*- coding: utf-8 -*- | |
| import json | |
| import requests | |
| import re | |
| import random | |
| import xml.etree.ElementTree as ET | |
| from config import GEMINI_API_BASE, GEMINI_MODEL | |
| import settings_manager | |
| def verify_and_clean_urls(text): | |
| """ | |
| Проверяет все найденные в тексте ссылки. | |
| Если ссылка отдает 404/ошибку, она удаляется из текста. | |
| Также удаляет Markdown-форматирование ссылок. | |
| """ | |
| if not text: | |
| return text | |
| # Удаляем markdown-форматирование ссылок [text](url) -> url | |
| import re | |
| cleaned_text = re.sub(r'\[([^\]]+)\]\((https?://[^\)]+)\)', r'\2', text) | |
| urls = re.findall(r'(https?://[^\s)\]]+)', cleaned_text) | |
| if not urls: | |
| return cleaned_text | |
| for url in urls: | |
| try: | |
| headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} | |
| r = requests.head(url, headers=headers, timeout=5, allow_redirects=True) | |
| if r.status_code >= 400 and r.status_code not in [403, 401]: | |
| print(f"[Content Generator] Вилучено неробоче посилання: {url} (HTTP {r.status_code})") | |
| cleaned_text = cleaned_text.replace(url, "") | |
| else: | |
| print(f"[Content Generator] Посилання підтверджено: {url}") | |
| except Exception as e: | |
| print(f"[Content Generator] Вилучено неробоче посилання: {url} (Помилка: {e})") | |
| cleaned_text = cleaned_text.replace(url, "") | |
| return cleaned_text.strip() | |
| def smart_truncate(text, max_len=490): | |
| """ | |
| Умное усечение строки до max_len символов, сохраняя целостность ссылки в конце. | |
| """ | |
| if not text or len(text) <= max_len: | |
| return text | |
| import re | |
| urls = list(re.finditer(r'(https?://[^\s]+)', text)) | |
| if not urls: | |
| cut_point = max_len - 3 | |
| space_idx = text.rfind(' ', max(0, cut_point - 20), cut_point) | |
| if space_idx != -1: | |
| return text[:space_idx].strip() + "..." | |
| return text[:cut_point].strip() + "..." | |
| last_url = urls[-1] | |
| url_start, url_end = last_url.span() | |
| url_text = text[url_start:url_end] | |
| suffix = text[url_end:] | |
| prefix = text[:url_start] | |
| max_prefix_len = max_len - len(url_text) - len(suffix) - 3 | |
| if max_prefix_len >= 0: | |
| prefix_cut = max_prefix_len | |
| space_idx = prefix.rfind(' ', max(0, prefix_cut - 20), prefix_cut) | |
| if space_idx != -1: | |
| prefix_truncated = prefix[:space_idx].strip() | |
| else: | |
| prefix_truncated = prefix[:prefix_cut].strip() | |
| if prefix_truncated: | |
| return prefix_truncated + "..." + url_text + suffix | |
| return "..." + url_text + suffix | |
| else: | |
| cut_point = max_len - 3 | |
| space_idx = prefix.rfind(' ', max(0, cut_point - 20), cut_point) | |
| if space_idx != -1: | |
| return prefix[:space_idx].strip() + "..." | |
| return prefix[:cut_point].strip() + "..." | |
| def generate_text_via_gemini(prompt): | |
| """ | |
| Утилитарный метод для вызова нашего локального прокси-сервера Gemini. | |
| """ | |
| headers = {"Content-Type": "application/json"} | |
| payload = { | |
| "model": GEMINI_MODEL, | |
| "messages": [{"role": "user", "content": prompt}], | |
| "temperature": 0.7 | |
| } | |
| try: | |
| response = requests.post(f"{GEMINI_API_BASE}/chat/completions", headers=headers, json=payload, timeout=90) | |
| if response.status_code == 200: | |
| result = response.json() | |
| return result["choices"][0]["message"]["content"].strip() | |
| else: | |
| print(f"[Content Generator Error] API вернул код {response.status_code}: {response.text}") | |
| return None | |
| except Exception as e: | |
| print(f"[Content Generator Exception] Исключение при обращении к Gemini: {e}") | |
| return None | |
| def clean_json_response(content): | |
| """ | |
| Очищает ответ от Markdown-тегов ```json ... ``` и возвращает спарсенный словарь. | |
| """ | |
| if not content: | |
| return None | |
| try: | |
| content_clean = content.strip() | |
| if content_clean.startswith("```"): | |
| content_clean = content_clean.split("```")[1] | |
| if content_clean.startswith("json"): | |
| content_clean = content_clean[4:].strip() | |
| content_clean = content_clean.strip("`").strip() | |
| return json.loads(content_clean) | |
| except Exception as e: | |
| print(f"[Content Generator Error] Ошибка парсинга JSON: {e}. Сырой контент: {content}") | |
| return None | |
| def fetch_latest_news_and_trends(exclude_posts=None): | |
| """ | |
| Збирає свіжі новини через RSS для гарантії реальних, не вигаданих посилань. | |
| """ | |
| print("[Content Generator] Поиск реальных новостей через RSS-ленты...") | |
| urls = [ | |
| ("AI & IT", "https://techcrunch.com/category/artificial-intelligence/feed/"), | |
| ("Crypto & Web3", "https://cointelegraph.com/rss"), | |
| ("Business & Finance", "https://finance.yahoo.com/news/rss") | |
| ] | |
| def is_duplicate(title, exclude_list): | |
| if not exclude_list: | |
| return False | |
| title_lower = title.lower() | |
| words = re.findall(r'\b[a-zA-Z0-9]{4,}\b', title_lower) | |
| stopwords = {"with", "from", "that", "this", "news", "updates", "features", "unveils", "announces", "introduces"} | |
| keywords = [w for w in words if w not in stopwords] | |
| for post in exclude_list: | |
| post_lower = post.lower() | |
| match_count = 0 | |
| for kw in keywords: | |
| if kw in post_lower: | |
| match_count += 1 | |
| if match_count >= 2 or kw in ["lockdown", "siri", "wwdc", "waymo", "spacex"]: | |
| print(f"[Content Generator Filter] Excluded duplicate news: '{title}' (matched keyword '{kw}')") | |
| return True | |
| return False | |
| news_items = [] | |
| for category, url in urls: | |
| try: | |
| r = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10) | |
| if r.status_code == 200: | |
| root = ET.fromstring(r.text) | |
| items = root.findall(".//item") | |
| added_count = 0 | |
| for item in items: | |
| if added_count >= 2: | |
| break | |
| title = item.find("title").text if item.find("title") is not None else "" | |
| link = item.find("link").text if item.find("link") is not None else "" | |
| desc = item.find("description").text if item.find("description") is not None else "" | |
| # Очищаем HTML из описания | |
| desc_clean = re.sub(r'<[^>]+>', '', desc)[:300] + "..." | |
| if title and link: | |
| if is_duplicate(title, exclude_posts): | |
| continue | |
| news_items.append(f"Категория: {category}\nЗаголовок: {title}\nURL: {link}\nОписание: {desc_clean}\n") | |
| added_count += 1 | |
| except Exception as e: | |
| print(f"[Content Generator Error] Ошибка RSS для {category}: {e}") | |
| if not news_items: | |
| print("[Content Generator Error] Не удалось собрать новости по RSS.") | |
| return None | |
| return "\\n".join(news_items) | |
| def generate_single_post(news_context, exclude_posts=None): | |
| """ | |
| Генерирует одиночный пост с внедренными навыками (Personas) дизайнера и копирайтера. | |
| """ | |
| tones = [ | |
| "Провокативний стратег (гарячі тейки, злам стереотипів)", | |
| "Глибокий аналітик (розкладає по поличках складні речі)", | |
| "Інсайдер-візіонер (розповідає про майбутнє як про доконаний факт)" | |
| ] | |
| import random | |
| tone = random.choice(tones) | |
| exclude_str = "" | |
| if exclude_posts: | |
| exclude_items = [] | |
| for i, post_text in enumerate(exclude_posts): | |
| post_preview = post_text.replace('\n', ' ').strip()[:150] | |
| exclude_items.append(f"- {post_preview}...") | |
| exclude_list_str = "\n".join(exclude_items) | |
| exclude_str = f"\nКАТЕГОРИЧНО ЗАБОРОНЕНО обирати теми або новини, про які вже було опубліковано останні пости:\n{exclude_list_str}\nБудь ласка, обери повністю іншу новину з контексту, яка не має жодного відношення до вищеперелічених тем та новин!" | |
| settings = settings_manager.load_settings() | |
| prompt_template = settings.get("PROMPT_SINGLE", "") | |
| prompt = prompt_template.replace("{news_context}", str(news_context)).replace("{exclude_str}", str(exclude_str)).replace("{tone}", str(tone)) | |
| print("[Content Generator] Генерация одиночного поста (Persona Skills)...") | |
| raw_res = generate_text_via_gemini(prompt) | |
| data = clean_json_response(raw_res) | |
| if data and "description" in data: | |
| data["description"] = verify_and_clean_urls(data["description"]) | |
| return data | |
| def generate_carousel_post(news_context, exclude_posts=None): | |
| """ | |
| Генерирует карусель из 5 слайдов с внедренными навыками. | |
| """ | |
| tones = [ | |
| "Провокативний стратег (гарячі тейки, злам стереотипів)", | |
| "Глибокий аналітик (розкладає по поличках складні речі)", | |
| "Інсайдер-візіонер (розповідає про майбутнє как про доконаний факт)" | |
| ] | |
| import random | |
| tone = random.choice(tones) | |
| exclude_str = "" | |
| if exclude_posts: | |
| exclude_items = [] | |
| for i, post_text in enumerate(exclude_posts): | |
| post_preview = post_text.replace('\n', ' ').strip()[:150] | |
| exclude_items.append(f"- {post_preview}...") | |
| exclude_list_str = "\n".join(exclude_items) | |
| exclude_str = f"\nКАТЕГОРИЧНО ЗАБОРОНЕНО обирати теми або новини, про які вже було опубліковано останні пости:\n{exclude_list_str}\nБудь ласка, обери повністю іншу новину з контексту, яка не має жодного відношення до вищеперелічених тем та новин!" | |
| settings = settings_manager.load_settings() | |
| prompt_template = settings.get("PROMPT_CAROUSEL", "") | |
| slide_count = settings.get("CAROUSEL_SLIDE_COUNT", 5) | |
| prompt = prompt_template.replace("{news_context}", str(news_context)).replace("{exclude_str}", str(exclude_str)).replace("{tone}", str(tone)).replace("{slide_count}", str(slide_count)) | |
| print("[Content Generator] Генерация поста-карусели (Persona Skills)...") | |
| raw_res = generate_text_via_gemini(prompt) | |
| data = clean_json_response(raw_res) | |
| if data and "description" in data: | |
| data["description"] = verify_and_clean_urls(data["description"]) | |
| return data | |
| def predict_post_success(post_text, historical_data=None): | |
| """ | |
| Оценивает черновик поста на виральность (от 1 до 10). | |
| Если балл < 7, возвращает советы по улучшению. | |
| """ | |
| from config import GEMINI_MODEL, GEMINI_API_BASE | |
| import requests | |
| import json | |
| context = "" | |
| if historical_data: | |
| history_list = "\n".join([f"- Пост (Лайки: {p.get('likes', 0)}): {p.get('text')[:100]}..." for p in historical_data]) | |
| context = f"Для справки, вот наши самые успешные посты в прошлом:\n{history_list}\n" | |
| settings = settings_manager.load_settings() | |
| prompt_template = settings.get("PROMPT_PREDICTOR", "") | |
| prompt = prompt_template.replace("{context}", str(context)).replace("{post_text}", str(post_text)) | |
| headers = {"Content-Type": "application/json"} | |
| payload = { | |
| "model": GEMINI_MODEL, | |
| "messages": [{"role": "user", "content": prompt}], | |
| "temperature": 0.3 | |
| } | |
| try: | |
| response = requests.post(f"{GEMINI_API_BASE}/chat/completions", headers=headers, json=payload, timeout=30) | |
| if response.status_code == 200: | |
| result_json = response.json() | |
| content = result_json["choices"][0]["message"]["content"].strip() | |
| if content.startswith("```"): | |
| content = content.split("```")[1] | |
| if content.startswith("json"): | |
| content = content[4:].strip() | |
| content = content.strip("`").strip() | |
| parsed = json.loads(content) | |
| return parsed | |
| else: | |
| print(f"[Predictor Error] Ошибка Gemini API ({response.status_code})") | |
| return {"score": 7, "criticism": ""} | |
| except Exception as e: | |
| print(f"[Predictor Exception] Ошибка: {e}") | |
| return {"score": 7, "criticism": ""} | |