Automation_Threads_EU / content_generator.py
FincH12's picture
Upload content_generator.py with huggingface_hub
a3d8ff3 verified
Raw History Blame Contribute Delete
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": ""}