Nanny7 commited on
Commit
8587347
·
1 Parent(s): 9b7783e

feat: add /story/image endpoint with Unsplash→Pexels→Pollinations fallback + image API keys in config

Browse files
Files changed (4) hide show
  1. .env.example +6 -0
  2. app.py +59 -3
  3. config.py +6 -0
  4. static/js/stories-flashcards.js +34 -12
.env.example CHANGED
@@ -14,6 +14,12 @@ NVIDIA_API_KEY=
14
  GROQ_API_KEY=
15
  GROQ_API_KEYS=
16
 
 
 
 
 
 
 
17
  # Auth / local DB
18
  DATABASE_PATH=users.db
19
  JWT_SECRET_KEY=replace_with_a_long_random_secret_min_32_chars
 
14
  GROQ_API_KEY=
15
  GROQ_API_KEYS=
16
 
17
+ # Image APIs (story illustrations)
18
+ UNSPLASH_ACCESS_KEY=
19
+ PEXELS_API_KEY=
20
+ IMGBB_API_KEY=
21
+ POLLINATIONS_API_KEY=
22
+
23
  # Auth / local DB
24
  DATABASE_PATH=users.db
25
  JWT_SECRET_KEY=replace_with_a_long_random_secret_min_32_chars
app.py CHANGED
@@ -4,6 +4,7 @@ import time
4
  import json
5
  import random
6
  import tempfile
 
7
  from collections import defaultdict
8
  from flask import (
9
  Flask,
@@ -545,9 +546,15 @@ def generate_story():
545
  level = data.get("level", "intermediate")
546
  topic = data.get("topic", "daily life")
547
  lang_names = {
548
- "en": "English", "fr": "French", "es": "Spanish",
549
- "de": "German", "ar": "Arabic", "it": "Italian",
550
- "pt": "Portuguese", "ja": "Japanese", "zh": "Chinese",
 
 
 
 
 
 
551
  }
552
  lang_name = lang_names.get(language, "English")
553
  word_counts = {"beginner": 80, "intermediate": 130, "advanced": 180}
@@ -584,6 +591,55 @@ def generate_story():
584
  return jsonify({"error": "Could not generate story"}), 500
585
 
586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  def get_welcome_message(topic="free", language="en", scenario=None):
588
  lang_config = Config.LANGUAGES.get(language, Config.LANGUAGES["en"])
589
 
 
4
  import json
5
  import random
6
  import tempfile
7
+ import requests as _http
8
  from collections import defaultdict
9
  from flask import (
10
  Flask,
 
546
  level = data.get("level", "intermediate")
547
  topic = data.get("topic", "daily life")
548
  lang_names = {
549
+ "en": "English",
550
+ "fr": "French",
551
+ "es": "Spanish",
552
+ "de": "German",
553
+ "ar": "Arabic",
554
+ "it": "Italian",
555
+ "pt": "Portuguese",
556
+ "ja": "Japanese",
557
+ "zh": "Chinese",
558
  }
559
  lang_name = lang_names.get(language, "English")
560
  word_counts = {"beginner": 80, "intermediate": 130, "advanced": 180}
 
591
  return jsonify({"error": "Could not generate story"}), 500
592
 
593
 
594
+ @app.route("/story/image", methods=["POST"])
595
+ def get_story_image():
596
+ """Fetch a relevant image from Unsplash → Pexels → Pollinations fallback chain."""
597
+ data = request.json or {}
598
+ topic = str(data.get("topic", ""))[:60]
599
+ image_prompt = str(data.get("image_prompt", topic))[:80]
600
+ query = topic or image_prompt
601
+
602
+ # 1. Unsplash (highest quality, free API)
603
+ if Config.UNSPLASH_ACCESS_KEY:
604
+ try:
605
+ r = _http.get(
606
+ "https://api.unsplash.com/photos/random",
607
+ params={
608
+ "query": query,
609
+ "orientation": "landscape",
610
+ "count": 1,
611
+ "content_filter": "high",
612
+ },
613
+ headers={"Authorization": f"Client-ID {Config.UNSPLASH_ACCESS_KEY}"},
614
+ timeout=5,
615
+ )
616
+ if r.ok:
617
+ photos = r.json()
618
+ if isinstance(photos, list) and photos:
619
+ return jsonify({"url": photos[0]["urls"]["regular"], "source": "unsplash"})
620
+ except Exception as e:
621
+ logging.warning(f"Unsplash image fetch failed: {e}")
622
+
623
+ # 2. Pexels fallback
624
+ if Config.PEXELS_API_KEY:
625
+ try:
626
+ r = _http.get(
627
+ "https://api.pexels.com/v1/search",
628
+ params={"query": query, "per_page": 1, "orientation": "landscape"},
629
+ headers={"Authorization": Config.PEXELS_API_KEY},
630
+ timeout=5,
631
+ )
632
+ if r.ok:
633
+ d = r.json()
634
+ if d.get("photos"):
635
+ return jsonify({"url": d["photos"][0]["src"]["large"], "source": "pexels"})
636
+ except Exception as e:
637
+ logging.warning(f"Pexels image fetch failed: {e}")
638
+
639
+ # 3. Signal client to use Pollinations (no server-side key needed)
640
+ return jsonify({"url": None, "source": "pollinations"})
641
+
642
+
643
  def get_welcome_message(topic="free", language="en", scenario=None):
644
  lang_config = Config.LANGUAGES.get(language, Config.LANGUAGES["en"])
645
 
config.py CHANGED
@@ -10,6 +10,12 @@ class Config:
10
  # --- API Keys ---
11
  NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
12
 
 
 
 
 
 
 
13
  # Groq keys with rotation support
14
  GROQ_API_KEYS = [
15
  k for k in [
 
10
  # --- API Keys ---
11
  NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
12
 
13
+ # Image API keys
14
+ UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY", "")
15
+ PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "")
16
+ IMGBB_API_KEY = os.environ.get("IMGBB_API_KEY", "")
17
+ POLLINATIONS_API_KEY = os.environ.get("POLLINATIONS_API_KEY", "")
18
+
19
  # Groq keys with rotation support
20
  GROQ_API_KEYS = [
21
  k for k in [
static/js/stories-flashcards.js CHANGED
@@ -215,18 +215,40 @@ window.EchoExtra = (function () {
215
  });
216
  }
217
 
218
- // Image via Pollinations
219
- if (imgEl && data.image_prompt) {
220
- const imgUrl = pollinationsUrl(data.image_prompt);
221
- imgEl.onload = () => {
222
- if (shimmer) shimmer.classList.add('hidden');
223
- imgEl.classList.remove('hidden');
224
- };
225
- imgEl.onerror = () => {
226
- if (shimmer) shimmer.classList.add('hidden');
227
- };
228
- imgEl.src = imgUrl;
229
- imgEl.alt = data.title || 'Story illustration';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  } else {
231
  if (shimmer) shimmer.classList.add('hidden');
232
  }
 
215
  });
216
  }
217
 
218
+ // Image via backend (Unsplash → Pexels → Pollinations fallback)
219
+ if (imgEl && (data.image_prompt || this.currentTopic)) {
220
+ fetch('/story/image', {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ topic: this.currentTopic,
225
+ image_prompt: data.image_prompt || data.title || this.currentTopic,
226
+ }),
227
+ })
228
+ .then((r) => r.json())
229
+ .then((imgData) => {
230
+ const url = imgData.url || pollinationsUrl(data.image_prompt || this.currentTopic);
231
+ imgEl.alt = data.title || 'Story illustration';
232
+ imgEl.onload = () => {
233
+ if (shimmer) shimmer.classList.add('hidden');
234
+ imgEl.classList.remove('hidden');
235
+ };
236
+ imgEl.onerror = () => {
237
+ if (shimmer) shimmer.classList.add('hidden');
238
+ };
239
+ imgEl.src = url;
240
+ })
241
+ .catch(() => {
242
+ // Pollinations client-side fallback on network error
243
+ const url = pollinationsUrl(data.image_prompt || this.currentTopic);
244
+ imgEl.alt = data.title || 'Story illustration';
245
+ imgEl.onload = () => {
246
+ if (shimmer) shimmer.classList.add('hidden');
247
+ imgEl.classList.remove('hidden');
248
+ };
249
+ imgEl.onerror = () => { if (shimmer) shimmer.classList.add('hidden'); };
250
+ imgEl.src = url;
251
+ });
252
  } else {
253
  if (shimmer) shimmer.classList.add('hidden');
254
  }