monk / app.py
hf-actions
feat: run twice daily (every 12 hours) - morning and evening posts
c8fca20
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()