M-hv1 commited on
Commit
3cc0c20
Β·
verified Β·
1 Parent(s): affdee6

Update core/api_clients.py

Browse files
Files changed (1) hide show
  1. 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
- async def generate_truly_free_image(prompt: str, out_path: str) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  """
718
- V16.6 -- HF Inference Image Generator with Round-Robin Token Rotation.
719
-
720
- Step-by-step execution:
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
- "[HF-Image] Token pool empty -- set HF_TOKEN1 through HF_TOKEN8. "
753
- "Skipping image generation for this chapter."
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