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()