Spaces:
Sleeping
Sleeping
| 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() | |