Spaces:
Running
Running
Update core/api_clients.py
Browse files- core/api_clients.py +216 -34
core/api_clients.py
CHANGED
|
@@ -713,44 +713,226 @@ async def _hf_infer_image_with_token(
|
|
| 713 |
return False
|
| 714 |
|
| 715 |
|
| 716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
"""
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
1. Guard: if no tokens are loaded (_HF_IMAGE_TOKENS is empty),
|
| 723 |
-
log an error and return False immediately.
|
| 724 |
-
|
| 725 |
-
2. Truncate `prompt` to <= HF_IMAGE_MAX_KEYWORDS (15) keywords
|
| 726 |
-
via _truncate_to_keywords(). The shortened text is sent as
|
| 727 |
-
the POST body {"inputs": "..."} -- NOT as a URL query string.
|
| 728 |
-
|
| 729 |
-
3. For each token in the pool (round-robin, starting from the
|
| 730 |
-
current cursor):
|
| 731 |
-
a. _acquire_hf_image_token() returns the next token and
|
| 732 |
-
advances the cursor atomically.
|
| 733 |
-
b. _hf_infer_image_with_token() is called:
|
| 734 |
-
- 503 model-loading: retried up to 3x / 5 s on the
|
| 735 |
-
SAME token before giving up on it.
|
| 736 |
-
- 429 rate-limit: returns False immediately; the
|
| 737 |
-
loop moves to the next token at once.
|
| 738 |
-
- Other failure: returns False; loop continues.
|
| 739 |
-
- Success (>= 30 KB saved): returns True.
|
| 740 |
-
c. On True, log success and return True to the caller.
|
| 741 |
-
d. On False, log which token failed and try the next one.
|
| 742 |
-
|
| 743 |
-
4. If all tokens are exhausted without success, log an error and
|
| 744 |
-
return False.
|
| 745 |
-
|
| 746 |
-
Returns True only when a valid image >= 30 KB has been saved.
|
| 747 |
-
Returns False (non-fatal) on complete failure -- the immersion
|
| 748 |
-
pipeline sets image_url="" for that chapter and continues.
|
| 749 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
if not _HF_IMAGE_TOKENS:
|
| 751 |
logger.error(
|
| 752 |
-
"[
|
| 753 |
-
"
|
| 754 |
)
|
| 755 |
return False
|
| 756 |
|
|
|
|
| 713 |
return False
|
| 714 |
|
| 715 |
|
| 716 |
+
|
| 717 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 718 |
+
#
|
| 719 |
+
# V18.5 -- HYBRID IMAGE SYSTEM
|
| 720 |
+
#
|
| 721 |
+
# Priority order:
|
| 722 |
+
# 1. Wikimedia Commons β real historical images, public domain, no key
|
| 723 |
+
# 2. Pollinations.ai β free AI generation, no key, no limit
|
| 724 |
+
# 3. HF Inference β paid fallback (existing system)
|
| 725 |
+
#
|
| 726 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 727 |
+
|
| 728 |
+
WIKIMEDIA_API: str = "https://en.wikipedia.org/w/api.php"
|
| 729 |
+
WIKIMEDIA_TIMEOUT: float = 15.0
|
| 730 |
+
MIN_WIKIMEDIA_BYTES: int = 20 * 1024 # 20 KB minimum
|
| 731 |
+
|
| 732 |
+
POLLINATIONS_URL: str = "https://image.pollinations.ai/prompt/{prompt}"
|
| 733 |
+
POLLINATIONS_TIMEOUT: float = 60.0
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
async def _search_wikimedia_image(search_query: str, out_path: str) -> bool:
|
| 737 |
"""
|
| 738 |
+
Search Wikimedia Commons for a relevant historical image.
|
| 739 |
+
Uses the Wikipedia opensearch + imageinfo APIs.
|
| 740 |
+
Returns True if a valid image >= 20 KB was saved.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
"""
|
| 742 |
+
try:
|
| 743 |
+
# Step 1: Find the most relevant Wikipedia article
|
| 744 |
+
async with httpx.AsyncClient(timeout=WIKIMEDIA_TIMEOUT) as client:
|
| 745 |
+
resp = await client.get(
|
| 746 |
+
WIKIMEDIA_API,
|
| 747 |
+
params={
|
| 748 |
+
"action": "query",
|
| 749 |
+
"list": "search",
|
| 750 |
+
"srsearch": search_query,
|
| 751 |
+
"srlimit": 1,
|
| 752 |
+
"format": "json",
|
| 753 |
+
},
|
| 754 |
+
headers={"User-Agent": "TitanImmersion/1.0 (educational)"},
|
| 755 |
+
)
|
| 756 |
+
if resp.status_code != 200:
|
| 757 |
+
logger.warning("[Wikimedia] Search HTTP %d", resp.status_code)
|
| 758 |
+
return False
|
| 759 |
+
|
| 760 |
+
results = resp.json().get("query", {}).get("search", [])
|
| 761 |
+
if not results:
|
| 762 |
+
logger.warning("[Wikimedia] No results for: %s", search_query[:80])
|
| 763 |
+
return False
|
| 764 |
+
|
| 765 |
+
page_title = results[0]["title"]
|
| 766 |
+
logger.info("[Wikimedia] Found article: %s", page_title)
|
| 767 |
+
|
| 768 |
+
# Step 2: Get images from that article
|
| 769 |
+
async with httpx.AsyncClient(timeout=WIKIMEDIA_TIMEOUT) as client:
|
| 770 |
+
resp = await client.get(
|
| 771 |
+
WIKIMEDIA_API,
|
| 772 |
+
params={
|
| 773 |
+
"action": "query",
|
| 774 |
+
"titles": page_title,
|
| 775 |
+
"prop": "images",
|
| 776 |
+
"imlimit": 10,
|
| 777 |
+
"format": "json",
|
| 778 |
+
},
|
| 779 |
+
headers={"User-Agent": "TitanImmersion/1.0 (educational)"},
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
pages = resp.json().get("query", {}).get("pages", {})
|
| 783 |
+
images = []
|
| 784 |
+
for page in pages.values():
|
| 785 |
+
for img in page.get("images", []):
|
| 786 |
+
title = img.get("title", "")
|
| 787 |
+
# Skip icons, logos, flags, small decorative images
|
| 788 |
+
if any(skip in title.lower() for skip in [
|
| 789 |
+
"flag", "icon", "logo", "map", "seal", "coat",
|
| 790 |
+
"stub", "commons", "wikimedia", ".svg"
|
| 791 |
+
]):
|
| 792 |
+
continue
|
| 793 |
+
if title.lower().endswith((".jpg", ".jpeg", ".png")):
|
| 794 |
+
images.append(title)
|
| 795 |
+
|
| 796 |
+
if not images:
|
| 797 |
+
logger.warning("[Wikimedia] No usable images for: %s", page_title)
|
| 798 |
+
return False
|
| 799 |
+
|
| 800 |
+
# Step 3: Get the actual URL for the first good image
|
| 801 |
+
async with httpx.AsyncClient(timeout=WIKIMEDIA_TIMEOUT) as client:
|
| 802 |
+
resp = await client.get(
|
| 803 |
+
WIKIMEDIA_API,
|
| 804 |
+
params={
|
| 805 |
+
"action": "query",
|
| 806 |
+
"titles": images[0],
|
| 807 |
+
"prop": "imageinfo",
|
| 808 |
+
"iiprop": "url|size",
|
| 809 |
+
"iiurlwidth": 800,
|
| 810 |
+
"format": "json",
|
| 811 |
+
},
|
| 812 |
+
headers={"User-Agent": "TitanImmersion/1.0 (educational)"},
|
| 813 |
+
)
|
| 814 |
+
|
| 815 |
+
pages = resp.json().get("query", {}).get("pages", {})
|
| 816 |
+
img_url = None
|
| 817 |
+
for page in pages.values():
|
| 818 |
+
info = page.get("imageinfo", [])
|
| 819 |
+
if info:
|
| 820 |
+
img_url = info[0].get("thumburl") or info[0].get("url")
|
| 821 |
+
break
|
| 822 |
+
|
| 823 |
+
if not img_url:
|
| 824 |
+
logger.warning("[Wikimedia] No URL for image: %s", images[0])
|
| 825 |
+
return False
|
| 826 |
+
|
| 827 |
+
# Step 4: Download the image
|
| 828 |
+
async with httpx.AsyncClient(timeout=WIKIMEDIA_TIMEOUT) as client:
|
| 829 |
+
resp = await client.get(
|
| 830 |
+
img_url,
|
| 831 |
+
headers={"User-Agent": "TitanImmersion/1.0 (educational)"},
|
| 832 |
+
follow_redirects=True,
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
if resp.status_code != 200:
|
| 836 |
+
logger.warning("[Wikimedia] Download HTTP %d", resp.status_code)
|
| 837 |
+
return False
|
| 838 |
+
|
| 839 |
+
img_bytes = resp.content
|
| 840 |
+
if len(img_bytes) < MIN_WIKIMEDIA_BYTES:
|
| 841 |
+
logger.warning("[Wikimedia] Image too small: %d B", len(img_bytes))
|
| 842 |
+
return False
|
| 843 |
+
|
| 844 |
+
with open(out_path, "wb") as fh:
|
| 845 |
+
fh.write(img_bytes)
|
| 846 |
+
|
| 847 |
+
logger.info(
|
| 848 |
+
"[Wikimedia] β
Saved %s (%.1f KB) from: %s",
|
| 849 |
+
out_path, len(img_bytes) / 1024, page_title,
|
| 850 |
+
)
|
| 851 |
+
return True
|
| 852 |
+
|
| 853 |
+
except Exception as exc:
|
| 854 |
+
logger.warning("[Wikimedia] Exception: %s", exc)
|
| 855 |
+
return False
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
async def _generate_via_pollinations(prompt: str, out_path: str) -> bool:
|
| 859 |
+
"""
|
| 860 |
+
Generate image via Pollinations.ai β free, no API key, no rate limit.
|
| 861 |
+
Returns True if a valid image >= 20 KB was saved.
|
| 862 |
+
"""
|
| 863 |
+
try:
|
| 864 |
+
safe = urllib.parse.quote(prompt[:500])
|
| 865 |
+
url = f"https://image.pollinations.ai/prompt/{safe}?width=800&height=600&nologo=true"
|
| 866 |
+
|
| 867 |
+
logger.info("[Pollinations] Requesting image | prompt_len=%d", len(prompt))
|
| 868 |
+
async with httpx.AsyncClient(timeout=POLLINATIONS_TIMEOUT) as client:
|
| 869 |
+
resp = await client.get(url, follow_redirects=True)
|
| 870 |
+
|
| 871 |
+
if resp.status_code != 200:
|
| 872 |
+
logger.warning("[Pollinations] HTTP %d", resp.status_code)
|
| 873 |
+
return False
|
| 874 |
+
|
| 875 |
+
img_bytes = resp.content
|
| 876 |
+
if len(img_bytes) < MIN_WIKIMEDIA_BYTES:
|
| 877 |
+
logger.warning("[Pollinations] Image too small: %d B", len(img_bytes))
|
| 878 |
+
return False
|
| 879 |
+
|
| 880 |
+
with open(out_path, "wb") as fh:
|
| 881 |
+
fh.write(img_bytes)
|
| 882 |
+
|
| 883 |
+
logger.info(
|
| 884 |
+
"[Pollinations] β
Saved %s (%.1f KB)",
|
| 885 |
+
out_path, len(img_bytes) / 1024,
|
| 886 |
+
)
|
| 887 |
+
return True
|
| 888 |
+
|
| 889 |
+
except Exception as exc:
|
| 890 |
+
logger.warning("[Pollinations] Exception: %s", exc)
|
| 891 |
+
return False
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
async def generate_truly_free_image(
|
| 895 |
+
prompt: str,
|
| 896 |
+
out_path: str,
|
| 897 |
+
search_query: str = "",
|
| 898 |
+
) -> bool:
|
| 899 |
+
"""
|
| 900 |
+
V18.5 -- Hybrid Image System.
|
| 901 |
+
|
| 902 |
+
Priority order:
|
| 903 |
+
1. Wikimedia Commons β real historical images, public domain, no key
|
| 904 |
+
Uses search_query (historical_anchor event + year) if provided.
|
| 905 |
+
2. Pollinations.ai β free AI generation, no key, no rate limit
|
| 906 |
+
Uses the cinematic_image_prompt.
|
| 907 |
+
3. HF Inference β paid fallback (existing token pool system)
|
| 908 |
+
|
| 909 |
+
Returns True when any provider saves a valid image.
|
| 910 |
+
Returns False (non-fatal) β pipeline sets image_url="" and continues.
|
| 911 |
+
"""
|
| 912 |
+
# ββ Stage 1: Wikimedia Commons ββββββββββββββββββββββββββββββββββ
|
| 913 |
+
if search_query.strip():
|
| 914 |
+
logger.info("[Image/Hybrid] Stage 1: Wikimedia | query='%.80s'", search_query)
|
| 915 |
+
ok = await _search_wikimedia_image(search_query, out_path)
|
| 916 |
+
if ok:
|
| 917 |
+
logger.info("[Image/Hybrid] β
Wikimedia success")
|
| 918 |
+
return True
|
| 919 |
+
logger.info("[Image/Hybrid] Wikimedia failed β trying Pollinations")
|
| 920 |
+
else:
|
| 921 |
+
logger.info("[Image/Hybrid] No search_query β skipping Wikimedia")
|
| 922 |
+
|
| 923 |
+
# ββ Stage 2: Pollinations.ai βββββββββββββββββββββββββββββββββββββ
|
| 924 |
+
logger.info("[Image/Hybrid] Stage 2: Pollinations | prompt_len=%d", len(prompt))
|
| 925 |
+
ok = await _generate_via_pollinations(prompt, out_path)
|
| 926 |
+
if ok:
|
| 927 |
+
logger.info("[Image/Hybrid] β
Pollinations success")
|
| 928 |
+
return True
|
| 929 |
+
logger.info("[Image/Hybrid] Pollinations failed β trying HF tokens")
|
| 930 |
+
|
| 931 |
+
# ββ Stage 3: HF Inference (existing paid system) βββββββββββββββββ
|
| 932 |
if not _HF_IMAGE_TOKENS:
|
| 933 |
logger.error(
|
| 934 |
+
"[Image/Hybrid] All stages failed β no HF tokens available. "
|
| 935 |
+
"Setting image_url='' for this chapter."
|
| 936 |
)
|
| 937 |
return False
|
| 938 |
|