Spaces:
Sleeping
Sleeping
File size: 20,469 Bytes
ca2863a be4feb4 e8bb202 ca2863a afb566d ca2863a be4feb4 ca2863a 5f2de2a ca2863a 204674e ca2863a 204674e ca2863a 204674e ca2863a 204674e ca2863a afb566d 8216f24 c8fca20 8216f24 afb566d 8216f24 afb566d 0020de5 b42d9d1 0020de5 b42d9d1 0020de5 b42d9d1 0020de5 5de9f75 0020de5 5de9f75 0020de5 5de9f75 0020de5 5de9f75 0020de5 5de9f75 0020de5 afb566d 0020de5 b42d9d1 0020de5 ca2863a 3513553 ca2863a b42d9d1 c8fca20 ca2863a c8fca20 ca2863a e8bb202 ca2863a e8bb202 ca2863a e8bb202 ca2863a e8bb202 ca2863a b42d9d1 ca2863a fca2085 5f2de2a ca2863a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
import os
import logging
from dotenv import load_dotenv
import asyncio
import atexit
import time
import gradio as gr
import threading
import requests
import socket
from dotenv import load_dotenv
load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
def _close_asyncio_loop():
try:
loop = asyncio.get_event_loop()
if loop.is_running():
try:
loop.call_soon_threadsafe(loop.stop)
except Exception:
pass
if not loop.is_closed():
try:
loop.close()
except Exception:
pass
except Exception:
pass
atexit.register(_close_asyncio_loop)
from generate_wisdom import generate_wisdom
from post_to_facebook import post_to_facebook
from generate_image import generate_image, generate_and_post
from retry_queue import start_worker_thread
def generate_and_optionally_post(topic: str, do_post: bool):
try:
logger.info("Generating wisdom for topic: %s", topic)
wisdom = generate_wisdom(topic)
except Exception as e:
logger.exception("Failed to generate wisdom: %s", e)
return "Error generating wisdom: " + str(e), ""
if do_post:
page_id = os.getenv("FB_PAGE_ID")
token = os.getenv("FB_PAGE_ACCESS_TOKEN")
if not page_id or not token:
logger.error("Missing Facebook credentials in environment")
return wisdom, "Missing FB_PAGE_ID or FB_PAGE_ACCESS_TOKEN in environment"
try:
logger.info("Posting wisdom to Facebook page %s", page_id)
res = post_to_facebook(page_id, token, wisdom)
status = f"Posted to Facebook: {res.get('id') if isinstance(res, dict) else res}"
except Exception as e:
logger.exception("Failed to post to Facebook: %s", e)
status = "Failed to post to Facebook: " + str(e)
else:
status = "Not posted (preview only)"
return wisdom, status
with gr.Blocks() as demo:
# If requested via env, show a simple log-only UI that displays `log.txt` entries
SHOW_LOG = os.getenv("SHOW_POST_LOG", "false").lower() in ("1", "true", "yes")
if SHOW_LOG:
gr.Markdown("# Posted Log")
log_box = gr.Textbox(label="Post Log (most recent first)", lines=20)
with gr.Row():
refresh_btn = gr.Button("Refresh")
clear_btn = gr.Button("Clear Log")
def read_log():
try:
if not os.path.exists("log.txt"):
return "(no log.txt present)"
with open("log.txt", "r", encoding="utf-8") as lf:
data = lf.read().strip()
# show most recent entries first for convenience
lines = data.splitlines()
return "\n".join(reversed(lines)) if lines else "(log empty)"
except Exception as e:
return f"Error reading log.txt: {e}"
def clear_log():
try:
open("log.txt", "w", encoding="utf-8").close()
return "(log cleared)"
except Exception as e:
return f"Error clearing log.txt: {e}"
refresh_btn.click(fn=read_log, inputs=[], outputs=[log_box])
clear_btn.click(fn=clear_log, inputs=[], outputs=[log_box])
# Populate initially
log_box.value = read_log()
else:
gr.Markdown("# Buddhism Wisdom Generator & Facebook Poster")
with gr.Row():
topic = gr.Textbox(label="Topic (optional)", placeholder="e.g. mindfulness, compassion")
do_post = gr.Checkbox(label="Post to Facebook", value=False)
with gr.Row():
image_prompt = gr.Textbox(label="Image prompt (optional)", placeholder="Prompt for image generation")
image_post = gr.Checkbox(label="Post image to Facebook", value=False)
force_replicate = gr.Checkbox(label="Force Replicate", value=False)
use_wisdom_for_image = gr.Checkbox(label="Use generated wisdom as image prompt", value=True)
generate_btn = gr.Button("Generate & Post")
output_text = gr.Textbox(label="Generated Wisdom", lines=3)
status = gr.Textbox(label="Status", lines=2)
img_btn = gr.Button("Generate Image & Post")
img_output = gr.Image(label="Generated Image")
img_status = gr.Textbox(label="Image Status", lines=2)
generate_btn.click(fn=generate_and_optionally_post, inputs=[topic, do_post], outputs=[output_text, status])
def _gen_img(prompt, do_post, use_wisdom, force_replicate):
if not prompt and not use_wisdom:
return None, "No image prompt provided and not using wisdom"
try:
provider_order = None
if force_replicate:
provider_order = "replicate,openai"
res = generate_and_post(prompt, caption=None, post=do_post, use_wisdom_as_prompt=use_wisdom, provider_order=provider_order)
img_path = res.get("image_path")
wisdom = res.get("wisdom")
fb = res.get("facebook")
status_text = "Saved: " + img_path
if wisdom:
status_text += " | Wisdom used: " + (wisdom if len(wisdom) < 120 else wisdom[:117] + "...")
if fb:
status_text += " | Posted: " + str(fb)
return img_path, status_text
except Exception as e:
return None, "Error: " + str(e)
img_btn.click(fn=_gen_img, inputs=[image_prompt, image_post, use_wisdom_for_image, force_replicate], outputs=[img_output, img_status])
if __name__ == "__main__":
# Before launching, optionally start the daily Replicate job if enabled
def validate_tokens() -> bool:
"""Validate that required tokens are present and usable."""
load_dotenv()
fb_token = os.getenv("FB_PAGE_ACCESS_TOKEN")
fb_page = os.getenv("FB_PAGE_ID")
rep_token = os.getenv("REPLICATE_API_TOKEN")
ok = True
if not fb_token or not fb_page:
logger.error("Facebook token or page id missing")
ok = False
else:
# quick DNS resolution check to avoid noisy stack traces when DNS fails
# Retry DNS resolution a few times with exponential backoff to handle transient name-resolution blips
dns_attempts = int(os.getenv("FB_DNS_RETRY_ATTEMPTS", "5"))
dns_backoff_base = int(os.getenv("FB_DNS_BACKOFF_BASE", "2"))
dns_ok = False
last_dns_exc = None
for attempt in range(1, dns_attempts + 1):
try:
socket.getaddrinfo("graph.facebook.com", 443)
dns_ok = True
break
except Exception as e:
last_dns_exc = e
logger.warning("DNS resolution attempt %s/%s failed: %s", attempt, dns_attempts, e)
if attempt < dns_attempts:
try:
time.sleep(dns_backoff_base * (2 ** (attempt - 1)))
except Exception:
pass
if not dns_ok:
logger.error("DNS resolution failed for graph.facebook.com after %s attempts: %s", dns_attempts, last_dns_exc)
try:
with open("log.txt", "a", encoding="utf-8") as lf:
lf.write(f"[{__import__('time').strftime('%Y-%m-%d %H:%M:%S')}] FB_DNS_RESOLUTION_FAILED attempts={dns_attempts} error={last_dns_exc}\n")
except Exception:
logger.exception("Failed to write DNS failure to log.txt")
ok = False
# skip further FB checks when DNS is not available
return ok
# Try to inspect the token using the Graph API debug_token endpoint when possible
# This provides detailed info (is_valid, expires_at, type) when app credentials are available
fb_app_id = os.getenv("FB_APP_ID")
fb_app_secret = os.getenv("FB_APP_SECRET")
debug_info = None
try:
if fb_app_id and fb_app_secret:
app_access = f"{fb_app_id}|{fb_app_secret}"
dbg_url = f"https://graph.facebook.com/debug_token?input_token={fb_token}&access_token={app_access}"
rdbg = requests.get(dbg_url, timeout=10)
if rdbg.status_code == 200:
debug_info = rdbg.json().get("data")
else:
logger.warning("Facebook debug_token returned %s: %s", rdbg.status_code, rdbg.text)
# If debug_token not used or failed, fall back to /me check
if debug_info is None:
r = requests.get(f"https://graph.facebook.com/me?access_token={fb_token}", timeout=10)
if r.status_code == 200:
debug_info = {"is_valid": True}
else:
# log detailed failure
reason = f"/me check returned {r.status_code}: {r.text}"
logger.warning("Facebook token validation returned %s: %s", r.status_code, r.text)
try:
with open("log.txt", "a", encoding="utf-8") as lf:
lf.write(f"[{__import__('time').strftime('%Y-%m-%d %H:%M:%S')}] FB_TOKEN_VALIDATION_ERROR reason={reason}\n")
except Exception:
logger.exception("Failed to write token validation failure to log.txt")
ok = False
# If we have debug_info, examine it for validity and expiration
if debug_info:
is_valid = debug_info.get("is_valid", False)
if not is_valid:
reason = f"debug_token reports invalid: {debug_info}"
logger.error(reason)
try:
with open("log.txt", "a", encoding="utf-8") as lf:
lf.write(f"[{__import__('time').strftime('%Y-%m-%d %H:%M:%S')}] FB_TOKEN_VALIDATION_ERROR reason={reason}\n")
except Exception:
logger.exception("Failed to write token validation failure to log.txt")
ok = False
else:
# determine expiry classification if available and log remaining days
expires_at = debug_info.get("expires_at")
remaining_days = None
if expires_at:
try:
expires_at = int(expires_at)
import time as _time
delta = expires_at - int(_time.time())
remaining_days = max(0, delta // 86400)
if remaining_days >= 30:
token_type = f"long-lived (~{remaining_days} days)"
else:
token_type = f"short-lived (~{remaining_days} days)"
except Exception:
token_type = "unknown-expiry"
else:
token_type = "non-expiring-or-page-token"
logger.info("Facebook token looks valid (%s)", token_type)
try:
with open("log.txt", "a", encoding="utf-8") as lf:
entry = f"[{__import__('time').strftime('%Y-%m-%d %H:%M:%S')}] FB_TOKEN_VALIDATION_OK type={token_type}"
if remaining_days is not None:
entry += f" remaining_days={remaining_days}"
entry += "\n"
lf.write(entry)
except Exception:
logger.exception("Failed to write token validation ok to log.txt")
except requests.exceptions.RequestException as e:
# network-related errors (including DNS/name resolution) are common in Spaces intermittently
logger.exception("Facebook token validation network failure: %s", e)
try:
with open("log.txt", "a", encoding="utf-8") as lf:
lf.write(f"[{__import__('time').strftime('%Y-%m-%d %H:%M:%S')}] FB_TOKEN_VALIDATION_NETWORK_EXCEPTION {e}\n")
except Exception:
logger.exception("Failed to write token validation exception to log.txt")
ok = False
except Exception as e:
logger.exception("Facebook token validation failed with exception")
try:
with open("log.txt", "a", encoding="utf-8") as lf:
lf.write(f"[{__import__('time').strftime('%Y-%m-%d %H:%M:%S')}] FB_TOKEN_VALIDATION_EXCEPTION {e}\n")
except Exception:
logger.exception("Failed to write token validation exception to log.txt")
ok = False
if not rep_token:
logger.error("Replicate token missing")
ok = False
else:
try:
r2 = requests.get("https://api.replicate.com/v1/models", headers={"Authorization": f"Token {rep_token}"}, timeout=10)
if r2.status_code not in (200, 401):
# 401 means token invalid - still a valid response shape
logger.warning("Replicate models check returned %s", r2.status_code)
# treat 200 as ok
if r2.status_code == 200:
pass
except Exception:
logger.exception("Replicate token validation failed")
ok = False
return ok
# Default to true so the Space runs daily by default; override via Space Secrets if needed
run_daily = os.getenv("RUN_DAILY_REPLICATE", "true").lower() in ("1", "true", "yes")
if run_daily:
logger.info("RUN_DAILY_REPLICATE enabled — validating tokens before starting background job")
force_run = os.getenv("FORCE_RUN_DAILY", "false").lower() in ("1", "true", "yes")
skip_initial_validation = os.getenv("SKIP_INITIAL_VALIDATION", "false").lower() in ("1", "true", "yes")
if validate_tokens() or force_run or skip_initial_validation:
def _bg():
interval_hours = int(os.getenv("DAILY_INTERVAL_HOURS", "12"))
startup_delay = int(os.getenv("SCHEDULER_STARTUP_DELAY", "30"))
if startup_delay > 0:
logger.info("Delaying first scheduled run by %s seconds to allow Space networking to stabilize", startup_delay)
time.sleep(startup_delay)
while True:
try:
logger.info("Starting scheduled daily generate_and_post run")
# validate tokens for each scheduled run; skip this run if validation fails
if not validate_tokens():
logger.error("Scheduled run skipped due to token validation failure")
logger.info("Sleeping for %s hours before next scheduled run", interval_hours)
time.sleep(interval_hours * 3600)
continue
# reuse existing generate_and_post helper: generate image (using provider order) and post
prompt = os.getenv("DAILY_PROMPT")
if not prompt:
prompt = '''Create a photorealistic, serene Buddhist scene at sunrise. A calm Buddha statue in side profile, seated in meditation on a stone platform. Soft golden morning light with gentle rim lighting. Misty mountains and ancient Buddhist temple stupas fading into the background, creating depth and a peaceful spiritual atmosphere. Subtle haze, warm earth tones, cinematic lighting, natural stone textures, shallow depth of field. A tranquil lotus flower near still reflective water in the foreground.
Generate a short, original Buddhist-style quote about mindfulness, impermanence, inner peace, or awareness. The quote must be calm, timeless, and simple (no modern language, no slang). Keep it under 18 words.
Overlay the generated quote text in the center of the image using an elegant serif or handwritten-style font. The text should be softly glowing, perfectly legible, minimal, and harmonious with the scene. Avoid bold, modern, or decorative typography.
Ultra-realistic photography style, fine-art cinematic composition, calming mood, high detail, balanced contrast, 4K quality.'''
try:
res = generate_and_post(prompt, caption=None, post=True, use_wisdom_as_prompt=True, caption_template=None, use_wisdom_as_caption=True)
logger.info("Scheduled run result: %s", res)
except Exception:
logger.exception("Scheduled run failed")
except Exception:
logger.exception("Scheduled background loop crashed")
logger.info("Sleeping for %s hours before next scheduled run", interval_hours)
time.sleep(interval_hours * 3600)
t = threading.Thread(target=_bg, daemon=True)
t.start()
logger.info("Started background daily generate_and_post thread")
else:
logger.error("Token validation failed — daily job will not start (set FORCE_RUN_DAILY=true to override)")
# Optionally run one immediate generation on startup (headless/autostart)
run_on_start = os.getenv("RUN_ON_START", "false").lower() in ("1", "true", "yes")
if run_on_start:
logger.info("RUN_ON_START enabled — validating tokens before immediate run")
if validate_tokens():
try:
prompt = os.getenv("DAILY_PROMPT")
if not prompt:
prompt = '''Create a photorealistic, serene Buddhist scene at sunrise. A calm Buddha statue in side profile, seated in meditation on a stone platform. Soft golden morning light with gentle rim lighting. Misty mountains and ancient Buddhist temple stupas fading into the background, creating depth and a peaceful spiritual atmosphere. Subtle haze, warm earth tones, cinematic lighting, natural stone textures, shallow depth of field. A tranquil lotus flower near still reflective water in the foreground.
Generate a short, original Buddhist-style quote about mindfulness, impermanence, inner peace, or awareness. The quote must be calm, timeless, and simple (no modern language, no slang). Keep it under 18 words.
Overlay the generated quote text in the center of the image using an elegant serif or handwritten-style font. The text should be softly glowing, perfectly legible, minimal, and harmonious with the scene. Avoid bold, modern, or decorative typography.
Ultra-realistic photography style, fine-art cinematic composition, calming mood, high detail, balanced contrast, 4K quality.'''
logger.info("Running one-time startup generate_and_post")
try:
res = generate_and_post(prompt, caption=None, post=True, use_wisdom_as_prompt=True, caption_template=None, use_wisdom_as_caption=True)
logger.info("Startup run result: %s", res)
except Exception:
logger.exception("Startup generate_and_post failed")
except Exception:
logger.exception("Immediate run crashed")
else:
logger.error("Token validation failed — immediate run will not start")
# start background retry worker for any enqueued failed posts
try:
start_worker_thread()
logger.info("Started retry queue worker thread")
except Exception:
logger.exception("Failed to start retry queue worker thread")
demo.launch()
|