Spaces:
Running
Running
fix(wall): conservative cleanup — only delete truly garbage posts, preserve FPT/auto_rewrite with valid images
Browse files- _wall_posts_for_view: VIEW-ONLY filtering (non-destructive) of placeholder
image auto posts (pollinations.ai, vnexpress logo, empty). Posts with valid
images are always kept. Sort newest-first, mixed sources.
- _post_has_slide_info: accept YouTube/TikTok/FB embed iframe videos as valid
slide-AI content (Short HOT oEmbed fallback case), not just /api/ai/short-file
- _wall_cleanup_once (startup): only persist-delete auto posts with NO valid
image AND no video AND no slides (true garbage). Never delete FPT embed posts
or auto_rewrite posts with real images — prevents data loss.
- /api/wall/cleanup: same conservative deletion logic
- app_v2_entry.py +67 -33
app_v2_entry.py
CHANGED
|
@@ -938,16 +938,23 @@ def _post_has_valid_image(p):
|
|
| 938 |
return False
|
| 939 |
|
| 940 |
def _post_has_slide_info(p):
|
| 941 |
-
"""True if the post has usable slide/rewrite AI content
|
| 942 |
-
|
| 943 |
-
|
|
|
|
|
|
|
| 944 |
slides = p.get("slides") or []
|
| 945 |
if any((s.get("image") or "").strip() and (s.get("text") or "").strip() for s in slides if isinstance(s, dict)):
|
| 946 |
return True
|
| 947 |
-
#
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
return True
|
| 952 |
return False
|
| 953 |
|
|
@@ -983,11 +990,14 @@ def _created_ts(p):
|
|
| 983 |
|
| 984 |
def _wall_posts_for_view():
|
| 985 |
"""Apply the user-facing wall filtering + sorting:
|
| 986 |
-
1)
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
'
|
| 990 |
-
|
|
|
|
|
|
|
|
|
|
| 991 |
posts = _load_wall_posts()
|
| 992 |
if not posts:
|
| 993 |
return []
|
|
@@ -995,11 +1005,10 @@ def _wall_posts_for_view():
|
|
| 995 |
for p in posts:
|
| 996 |
if not isinstance(p, dict):
|
| 997 |
continue
|
| 998 |
-
if _is_auto_post(p):
|
| 999 |
-
# auto post
|
| 1000 |
-
#
|
| 1001 |
-
|
| 1002 |
-
continue
|
| 1003 |
filtered.append(p)
|
| 1004 |
filtered.sort(key=_created_ts, reverse=True)
|
| 1005 |
return filtered
|
|
@@ -1244,10 +1253,14 @@ def api_wall_delete(post_id: str):
|
|
| 1244 |
|
| 1245 |
@app.post('/api/wall/cleanup')
|
| 1246 |
def api_wall_cleanup():
|
| 1247 |
-
"""Permanently remove auto-generated wall posts that
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
Returns the number of removed posts and the new
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1251 |
posts = _load_wall_posts()
|
| 1252 |
if not isinstance(posts, list):
|
| 1253 |
return JSONResponse({"error": "No posts"}, status_code=404)
|
|
@@ -1258,7 +1271,7 @@ def api_wall_cleanup():
|
|
| 1258 |
if not isinstance(p, dict):
|
| 1259 |
removed.append(str(p))
|
| 1260 |
continue
|
| 1261 |
-
if _is_auto_post(p) and not
|
| 1262 |
removed.append(p.get('id', '?'))
|
| 1263 |
else:
|
| 1264 |
kept.append(p)
|
|
@@ -2532,10 +2545,15 @@ def _scheduler_loop():
|
|
| 2532 |
threading.Thread(target=_scheduler_loop, daemon=True, name='auto-rewrite-scheduler').start()
|
| 2533 |
|
| 2534 |
def _wall_cleanup_once():
|
| 2535 |
-
"""One-time startup cleanup:
|
| 2536 |
-
|
| 2537 |
-
|
| 2538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2539 |
try:
|
| 2540 |
time.sleep(8)
|
| 2541 |
posts = _load_wall_posts()
|
|
@@ -2544,19 +2562,35 @@ def _wall_cleanup_once():
|
|
| 2544 |
kept = []
|
| 2545 |
removed = 0
|
| 2546 |
removed_ids = []
|
|
|
|
| 2547 |
for p in posts:
|
| 2548 |
if not isinstance(p, dict):
|
| 2549 |
removed += 1
|
| 2550 |
continue
|
| 2551 |
-
if _is_auto_post(p)
|
| 2552 |
-
|
| 2553 |
-
|
| 2554 |
-
|
| 2555 |
-
|
| 2556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2557 |
kept.sort(key=_created_ts, reverse=True)
|
| 2558 |
_save_wall_posts(kept)
|
| 2559 |
-
print(f"[wall] startup cleanup removed {removed}
|
| 2560 |
except Exception as e:
|
| 2561 |
print(f"[wall] startup cleanup error: {e}")
|
| 2562 |
|
|
|
|
| 938 |
return False
|
| 939 |
|
| 940 |
def _post_has_slide_info(p):
|
| 941 |
+
"""True if the post has usable slide/rewrite AI content:
|
| 942 |
+
- designed slides with text+image, OR
|
| 943 |
+
- a generated short video (/api/ai/short-file/) with a real thumbnail, OR
|
| 944 |
+
- an oEmbed iframe (YouTube/TikTok/Facebook embed) with a real thumbnail
|
| 945 |
+
(Short HOT fallback case — embeddable, viewable as a slide)."""
|
| 946 |
slides = p.get("slides") or []
|
| 947 |
if any((s.get("image") or "").strip() and (s.get("text") or "").strip() for s in slides if isinstance(s, dict)):
|
| 948 |
return True
|
| 949 |
+
# Generated short video with a real thumbnail
|
| 950 |
+
vid = (p.get("video") or "").strip()
|
| 951 |
+
if vid and "api/ai/short-file/" in vid and (p.get("short_thumb") or "").strip():
|
| 952 |
+
return True
|
| 953 |
+
# oEmbed iframe (YouTube/TikTok/FB) with a valid image -> viewable embed slide
|
| 954 |
+
if vid and ("youtube.com/embed" in vid or "tiktok.com/embed" in vid or
|
| 955 |
+
"facebook.com/plugins" in vid or "instagram.com" in vid or
|
| 956 |
+
"twitter.com" in vid or "x.com" in vid or "player.vimeo" in vid):
|
| 957 |
+
if _post_has_valid_image(p):
|
| 958 |
return True
|
| 959 |
return False
|
| 960 |
|
|
|
|
| 990 |
|
| 991 |
def _wall_posts_for_view():
|
| 992 |
"""Apply the user-facing wall filtering + sorting:
|
| 993 |
+
1) Hide auto-generated posts that have a wrong/placeholder image (no
|
| 994 |
+
valid image at all — pollinations.ai, vnexpress logo, empty) or have
|
| 995 |
+
no slide-AI content (no slides / no short-video / no embed). These
|
| 996 |
+
are the 'bài đăng tự động bị sai ảnh / ko có thông tin slide AI' that
|
| 997 |
+
don't match the original article. This filter is VIEW-ONLY — it does
|
| 998 |
+
NOT delete from the persistent store.
|
| 999 |
+
2) Sort newest-first by creation timestamp, mixing all sources (FPT,
|
| 1000 |
+
slide rewrite AI, etc.) without source separation."""
|
| 1001 |
posts = _load_wall_posts()
|
| 1002 |
if not posts:
|
| 1003 |
return []
|
|
|
|
| 1005 |
for p in posts:
|
| 1006 |
if not isinstance(p, dict):
|
| 1007 |
continue
|
| 1008 |
+
if _is_auto_post(p) and not (_post_has_valid_image(p) and _post_has_slide_info(p)):
|
| 1009 |
+
# auto post with a wrong/placeholder/missing image or no slide AI
|
| 1010 |
+
# content -> hide from the wall (non-destructive)
|
| 1011 |
+
continue
|
|
|
|
| 1012 |
filtered.append(p)
|
| 1013 |
filtered.sort(key=_created_ts, reverse=True)
|
| 1014 |
return filtered
|
|
|
|
| 1253 |
|
| 1254 |
@app.post('/api/wall/cleanup')
|
| 1255 |
def api_wall_cleanup():
|
| 1256 |
+
"""Permanently remove auto-generated wall posts that are completely
|
| 1257 |
+
unrecoverable (no valid image, no video, no slides, no embed — i.e. a
|
| 1258 |
+
post that can never be displayed), and sort the remaining posts
|
| 1259 |
+
newest-first. Returns the number of removed posts and the new count.
|
| 1260 |
+
|
| 1261 |
+
NOTE: Conservative — auto posts that HAVE a valid image or a video/embed
|
| 1262 |
+
are preserved (they remain visible at view time via
|
| 1263 |
+
_wall_posts_for_view). Only use this to purge genuine garbage."""
|
| 1264 |
posts = _load_wall_posts()
|
| 1265 |
if not isinstance(posts, list):
|
| 1266 |
return JSONResponse({"error": "No posts"}, status_code=404)
|
|
|
|
| 1271 |
if not isinstance(p, dict):
|
| 1272 |
removed.append(str(p))
|
| 1273 |
continue
|
| 1274 |
+
if _is_auto_post(p) and not _post_has_valid_image(p) and not _post_has_slide_info(p) and not (p.get("video") or "").strip():
|
| 1275 |
removed.append(p.get('id', '?'))
|
| 1276 |
else:
|
| 1277 |
kept.append(p)
|
|
|
|
| 2545 |
threading.Thread(target=_scheduler_loop, daemon=True, name='auto-rewrite-scheduler').start()
|
| 2546 |
|
| 2547 |
def _wall_cleanup_once():
|
| 2548 |
+
"""One-time startup cleanup: sort the persistent wall store newest-first
|
| 2549 |
+
and remove truly-garbage auto posts (no valid image at ALL, no video, no
|
| 2550 |
+
slides, no embed URL — i.e. a post that can never be displayed).
|
| 2551 |
+
|
| 2552 |
+
NOTE: This is intentionally conservative. Posts with a valid image or a
|
| 2553 |
+
valid embed/video are NEVER deleted here — they are only hidden at view
|
| 2554 |
+
time by _wall_posts_for_view() when the image is a placeholder. This
|
| 2555 |
+
prevents accidental data loss of FPT Short AI posts whose image is valid
|
| 2556 |
+
but may temporarily lack a short_thumb."""
|
| 2557 |
try:
|
| 2558 |
time.sleep(8)
|
| 2559 |
posts = _load_wall_posts()
|
|
|
|
| 2562 |
kept = []
|
| 2563 |
removed = 0
|
| 2564 |
removed_ids = []
|
| 2565 |
+
needs_sort = False
|
| 2566 |
for p in posts:
|
| 2567 |
if not isinstance(p, dict):
|
| 2568 |
removed += 1
|
| 2569 |
continue
|
| 2570 |
+
if _is_auto_post(p):
|
| 2571 |
+
has_img = _post_has_valid_image(p)
|
| 2572 |
+
has_content = _post_has_slide_info(p)
|
| 2573 |
+
vid = (p.get("video") or "").strip()
|
| 2574 |
+
# Only delete auto posts that are completely unrecoverable:
|
| 2575 |
+
# no valid image AND no video/slides/embed at all.
|
| 2576 |
+
if not has_img and not has_content and not vid:
|
| 2577 |
+
removed += 1
|
| 2578 |
+
removed_ids.append(p.get('id', '?'))
|
| 2579 |
+
continue
|
| 2580 |
+
kept.append(p)
|
| 2581 |
+
# Sort if any post lacks proper ordering
|
| 2582 |
+
prev_ts = None
|
| 2583 |
+
is_sorted = True
|
| 2584 |
+
for p in kept:
|
| 2585 |
+
ts = _created_ts(p)
|
| 2586 |
+
if prev_ts is not None and ts > prev_ts:
|
| 2587 |
+
is_sorted = False
|
| 2588 |
+
break
|
| 2589 |
+
prev_ts = ts
|
| 2590 |
+
if removed > 0 or not is_sorted:
|
| 2591 |
kept.sort(key=_created_ts, reverse=True)
|
| 2592 |
_save_wall_posts(kept)
|
| 2593 |
+
print(f"[wall] startup cleanup: removed {removed} garbage posts ({removed_ids[:10]}), kept {len(kept)}, sorted newest-first")
|
| 2594 |
except Exception as e:
|
| 2595 |
print(f"[wall] startup cleanup error: {e}")
|
| 2596 |
|