Spaces:
Sleeping
Sleeping
feat: add /story/image endpoint with Unsplash→Pexels→Pollinations fallback + image API keys in config
Browse files- .env.example +6 -0
- app.py +59 -3
- config.py +6 -0
- 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",
|
| 549 |
-
"
|
| 550 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|