fix: improve Threads screenshot quality — sizing, login overlay, dead code

- Platform-aware overlay sizing: 82% width for Threads (was 45% for all)
  so comment screenshots fill more of the output video
- scale_h: -1 → -2 across all overlay items for yuv420p compatibility
- Cookie validation in auth.py: detect stale Threads sessions and
  re-login automatically instead of screenshotting the login modal
- Remove ("button", "Log in") from popup dismissals — clicking Log in
  opens the auth flow, not a dismissal
- Fail-fast guard after popup dismissal: raise RuntimeError if login
  overlay is still visible, preventing bad screenshots
- Remove dead bgcolor/txtcolor assignments in screenshot.py

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pull/2556/head
Hong Phuc 6 days ago
parent add6ead41f
commit 290042a043

@ -13,6 +13,7 @@ from utils import settings
from utils.console import emit_scraper_event, print_substep
THREADS_LOGIN_URL = "https://www.threads.com/login"
THREADS_AUTH_CHECK_URL = "https://www.threads.net/"
THREADS_COOKIE_FILE = "./video_creation/data/cookie-threads.json"
DEFAULT_USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
@ -45,6 +46,22 @@ def _is_logged_in_threads_url(url: str) -> bool:
)
def _has_login_gate(page: Page) -> bool:
"""Detect Threads login modal overlaying the page."""
gate_selectors = [
'input[autocomplete="username"]',
'input[autocomplete="current-password"]',
'div[role="dialog"] button:has-text("Log in")',
]
for selector in gate_selectors:
try:
if page.locator(selector).first.is_visible():
return True
except Exception:
continue
return False
def login_to_threads(page: Page, _context: BrowserContext) -> None:
"""Log into Threads via Instagram credentials and persist session cookies."""
username = settings.config["threads"]["creds"].get("username", "").strip()
@ -131,6 +148,20 @@ def ensure_authenticated_context(browser: Browser, **kwargs) -> BrowserContext:
context.add_cookies(saved_cookies)
print_substep("Loaded saved Threads session cookies.")
emit_scraper_event("login", {"message": "Loaded saved session cookies"})
# Verify loaded cookies are still valid
page = context.new_page()
try:
page.goto(THREADS_AUTH_CHECK_URL, timeout=0)
page.wait_for_load_state("domcontentloaded")
page.wait_for_timeout(1500)
if _has_login_gate(page):
print_substep("Saved Threads cookies expired. Re-logging in...", style="yellow")
context.clear_cookies()
cookie_path.unlink(missing_ok=True)
login_to_threads(page, context)
finally:
page.close()
except (json.JSONDecodeError, IOError):
print_substep("Saved cookies corrupted. Logging in fresh...")
page = context.new_page()

@ -4,7 +4,7 @@ import re
from pathlib import Path
from typing import Final
from playwright.sync_api import ViewportSize
from playwright.sync_api import Page, ViewportSize
from platforms.threads.auth import ensure_authenticated_context
from utils import settings
@ -12,6 +12,28 @@ from utils.browser_backend import launch_browser
from utils.console import print_step, print_substep
def _dismiss_popups(page: Page) -> None:
"""Dismiss common Threads/Instagram overlays that block screenshots."""
dismissals = [
("[role='button']", "Not now"),
("button", "Allow all cookies"),
("[aria-label='Close']", None),
("div[role='dialog'] button", "Not Now"),
]
for selector, text in dismissals:
try:
if text:
el = page.locator(selector, has_text=text).first
else:
el = page.locator(selector).first
if el.count() and el.is_visible():
el.click()
page.wait_for_timeout(500)
print_substep(f"Dismissed popup: {selector}", style="dim")
except Exception:
pass
def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int) -> None:
"""
Downloads screenshots of Threads posts via Playwright.
@ -29,14 +51,7 @@ def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int)
thread_id = re.sub(r"[^\w\s-]", "", content_object["thread_id"])
Path(f"assets/temp/{thread_id}/png").mkdir(parents=True, exist_ok=True)
# Theme colors
theme = settings.config["settings"]["theme"]
if theme == "dark":
bgcolor = (33, 33, 36, 255)
txtcolor = (240, 240, 240)
else:
bgcolor = (255, 255, 255, 255)
txtcolor = (0, 0, 0)
# Device scale factor (higher resolution screenshots)
dsf = (W // 600) + 1
@ -56,6 +71,14 @@ def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int)
page.wait_for_load_state("networkidle")
page.wait_for_timeout(3000)
_dismiss_popups(page)
if page.locator('div[role="dialog"] button:has-text("Log in")').first.is_visible():
raise RuntimeError(
"Threads login overlay is blocking the page — saved cookies are stale. "
"Delete video_creation/data/cookie-threads.json and re-run."
)
postcontentpath = f"assets/temp/{thread_id}/png/title.png"
try:
# Threads.net uses div-based cards, not <article> elements.
@ -98,6 +121,12 @@ def get_screenshots_of_threads_posts(content_object: dict, screenshot_num: int)
if num_replies > 0:
# Scroll to load all replies inline on the post page
print_substep("Loading replies on post page...", style="dim")
_dismiss_popups(page)
if page.locator('div[role="dialog"] button:has-text("Log in")').first.is_visible():
raise RuntimeError(
"Threads login overlay is blocking the page — saved cookies are stale. "
"Delete video_creation/data/cookie-threads.json and re-run."
)
last_count = 0
stable_count = 0
for _ in range(20):

@ -307,10 +307,9 @@ def make_final_video(
console.log(f"[bold green] Video Will Be: {length} Seconds Long")
# --- Step 4: Build overlay items ---
screenshot_width = int((W * 45) // 100)
Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True)
platform = settings.config["settings"].get("platform", "reddit")
screenshot_width = int(W * (0.82 if platform == "threads" else 0.45))
Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True)
# Use actual screenshot for non-Reddit platforms (Threads etc.), Reddit template for Reddit
title_img_path = f"assets/temp/{reddit_id}/png/title.png"
@ -330,7 +329,7 @@ def make_final_video(
"duration": audio_clips_durations[0],
"opacity": opacity,
"scale_w": screenshot_width,
"scale_h": -1,
"scale_h": -2,
})
current_time += audio_clips_durations[0]
@ -344,7 +343,7 @@ def make_final_video(
"duration": audio_clips_durations[1] if len(audio_clips_durations) > 1 else 5,
"opacity": opacity,
"scale_w": screenshot_width,
"scale_h": -1,
"scale_h": -2,
})
elif settings.config["settings"]["storymodemethod"] == 1:
for i in range(number_of_clips):
@ -359,7 +358,7 @@ def make_final_video(
"duration": audio_clips_durations[dur_idx],
"opacity": opacity,
"scale_w": screenshot_width,
"scale_h": -1,
"scale_h": -2,
})
current_time += audio_clips_durations[dur_idx]
else:
@ -375,7 +374,7 @@ def make_final_video(
"duration": audio_clips_durations[dur_idx],
"opacity": opacity,
"scale_w": screenshot_width,
"scale_h": -1,
"scale_h": -2,
})
current_time += audio_clips_durations[dur_idx]

Loading…
Cancel
Save