gemel123 commited on
Commit
85c7ef6
·
verified ·
1 Parent(s): fafecbf

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +426 -0
app.py ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import pickle
4
+ import re
5
+ import tempfile
6
+ import numpy as np
7
+ import cv2
8
+ import gradio as gr
9
+ from PIL import Image
10
+ from sentence_transformers import SentenceTransformer
11
+ from transformers import AutoModelForImageClassification, AutoImageProcessor
12
+ from gradio_client import Client, handle_file
13
+ import openai
14
+ from flask import Flask, request, jsonify, Response
15
+ from flask_cors import CORS
16
+ import base64
17
+ import io
18
+
19
+ # ============================================================
20
+ # INITIALIZATION
21
+ # ============================================================
22
+ print("Initializing Tree & House Story Generator (Flask + Gradio)")
23
+
24
+ # OpenAI
25
+ client = openai.OpenAI(
26
+ base_url="https://api.llm7.io/v1",
27
+ api_key=os.environ.get("LLM7_API_KEY", "unused")
28
+ )
29
+
30
+ # NSFW MODEL
31
+ nsfw_processor = AutoImageProcessor.from_pretrained("Falconsai/nsfw_image_detection")
32
+ nsfw_model = AutoModelForImageClassification.from_pretrained("Falconsai/nsfw_image_detection")
33
+
34
+ # CLIP Interrogator
35
+ clipi_client = Client("https://fffiloni-clip-interrogator-2.hf.space/")
36
+
37
+ # SAFETY
38
+ UNSAFE_KEYWORDS = [
39
+ "penis", "vagina", "genitals", "testicle",
40
+ "sex", "sexual", "erotic", "porn", "explicit",
41
+ "nipple", "genital", "lewd"
42
+ ]
43
+
44
+ def is_safe_description(text):
45
+ text = text.lower()
46
+ return not any(re.search(rf"\b{w}\b", text) for w in UNSAFE_KEYWORDS)
47
+
48
+ def is_drawing_safe(image):
49
+ try:
50
+ inputs = nsfw_processor(images=image, return_tensors="pt")
51
+ outputs = nsfw_model(**inputs)
52
+ probs = outputs.logits.softmax(dim=1)[0]
53
+ if probs[1] > 0.85:
54
+ return False, "NSFW detected"
55
+ return True, None
56
+ except:
57
+ return True, None
58
+
59
+ # LOAD TREE & HOUSE MODEL
60
+ classifier = None
61
+ bert_model = None
62
+ tree_stories = []
63
+ house_stories = []
64
+
65
+ try:
66
+ with open("tree_house_model.pkl", "rb") as f:
67
+ model_package = pickle.load(f)
68
+
69
+ classifier = model_package["classifier"]
70
+ bert_model = SentenceTransformer(model_package["bert_model_name"])
71
+ tree_stories = model_package["tree_stories"]
72
+ house_stories = model_package["house_stories"]
73
+ model_accuracy = model_package["accuracy"]
74
+
75
+ print(f"✓ Model loaded ({model_accuracy*100:.2f}%)")
76
+ except:
77
+ print("Tree/House model not loaded")
78
+
79
+ # CORE FUNCTIONS
80
+ def get_image_description(image):
81
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
82
+ path = f.name
83
+ image.save(path)
84
+ try:
85
+ desc = clipi_client.predict(
86
+ image=handle_file(path),
87
+ mode="best",
88
+ api_name="/clipi2"
89
+ )
90
+ finally:
91
+ os.remove(path)
92
+ return desc
93
+
94
+ def predict_category(description):
95
+ if not classifier:
96
+ return None, 0.0, {}
97
+ emb = bert_model.encode([description], normalize_embeddings=True)
98
+ pred = classifier.predict(emb)[0]
99
+ probs = classifier.predict_proba(emb)[0]
100
+ return pred, float(max(probs)), {"tree": float(probs[0]), "house": float(probs[1])}
101
+
102
+ def get_random_story(category):
103
+ stories = tree_stories if category == "tree" else house_stories
104
+ if not stories:
105
+ return None
106
+ s = random.choice(stories).copy()
107
+ s.pop("category", None)
108
+ return s
109
+
110
+ def generate_story_openai(description, audience, is_vulgar=False):
111
+ """
112
+ description: user drawing description (string or list)
113
+ audience: string, e.g., 'children aged 5-7'
114
+ is_vulgar: boolean, True if Hugging Face classifier detects vulgar content
115
+ """
116
+
117
+ # Handle list input
118
+ if isinstance(description, list):
119
+ first_desc = description[0]
120
+ else:
121
+ first_desc = description
122
+
123
+ # Pre-check using local unsafe keywords
124
+ lower_desc = first_desc.lower()
125
+ for word in UNSAFE_KEYWORDS:
126
+ if word in lower_desc:
127
+ return "Your drawing contains inappropriate content and cannot be used to generate a story."
128
+
129
+ # If Hugging Face classifier detects vulgar content
130
+ if is_vulgar:
131
+ return "Your drawing contains inappropriate content and cannot be used to generate a story."
132
+
133
+ # Prompt with explicit instruction
134
+ prompt = (
135
+ f"Create a short, kid-friendly story for {audience} about: {first_desc}. "
136
+ f"Use simple, cheerful words suitable for children. Include characters, action, "
137
+ f"and make it imaginative. "
138
+ f"Write only 3 paragraphs. "
139
+ f"Also, provide a creative title at the very beginning. "
140
+ f"IMPORTANT: If the input contains any vulgar, sexual, or inappropriate content, "
141
+ f"do NOT generate any story and reply only with: "
142
+ f"'The content is inappropriate and cannot be used for a story.' "
143
+ f"Do NOT add extra questions, suggestions, or prompts at the end."
144
+ )
145
+
146
+ try:
147
+ res = client.chat.completions.create(
148
+ model="gpt-5-chat",
149
+ messages=[{"role": "user", "content": prompt}],
150
+ temperature=0.8
151
+ )
152
+ return res.choices[0].message.content
153
+ except:
154
+ return "Story generation failed."
155
+
156
+
157
+ def format_story(story_data):
158
+ """Return story text ONLY – no labels like characters, setting, plot, etc."""
159
+
160
+ if isinstance(story_data, str):
161
+ return story_data
162
+
163
+ elif isinstance(story_data, dict):
164
+ parts = []
165
+
166
+ # Optional title (pwede mo rin alisin kung ayaw mo)
167
+ if "title" in story_data and story_data["title"]:
168
+ parts.append(story_data["title"].strip())
169
+
170
+ # All possible story parts (NO LABELS)
171
+ ordered_keys = [
172
+ "characters",
173
+ "setting",
174
+ "plot",
175
+ "problem",
176
+ "ending",
177
+ "lesson",
178
+ "moral",
179
+ "story"
180
+ ]
181
+
182
+ for key in ordered_keys:
183
+ if key in story_data and story_data[key]:
184
+ parts.append(story_data[key].strip())
185
+
186
+ # Join as clean paragraphs
187
+ return "\n\n".join(parts)
188
+
189
+ else:
190
+ return str(story_data)
191
+
192
+ def process_image_base64(image_base64, audience="Children"):
193
+ image_data = base64.b64decode(image_base64)
194
+ image = Image.open(io.BytesIO(image_data)).convert("RGB")
195
+
196
+ # Description mula sa CLIP
197
+ description = get_image_description(image)
198
+
199
+ # Predict category (tree, house, other)
200
+ category, confidence, probs = predict_category(description)
201
+
202
+ # Check if description contains tree/house keywords
203
+ description_lower = description.lower()
204
+ is_tree = "tree" in description_lower or "branch" in description_lower or "leaf" in description_lower or "trunk" in description_lower
205
+ is_house = "house" in description_lower or "building" in description_lower or "home" in description_lower or "roof" in description_lower
206
+
207
+ # TREE/HOUSE → always use dataset story, ignore vulgar/NSFW
208
+ if (category == "tree" and is_tree) or (category == "house" and is_house):
209
+ story_data = get_random_story("tree" if category == "tree" else "house")
210
+ story_category = category
211
+ formatted_story = format_story(story_data)
212
+ return {
213
+ "success": True,
214
+ "description": description,
215
+ "category": story_category,
216
+ "confidence": confidence,
217
+ "probabilities": probs,
218
+ "story": formatted_story
219
+ }
220
+
221
+ # Ibang drawings → GPT-generated, check NSFW and vulgar keywords
222
+ safe, reason = is_drawing_safe(image)
223
+ if not safe:
224
+ return {"success": False, "error": "Bawal sa bata: NSFW detected", "reason": reason}
225
+
226
+ if not is_safe_description(description):
227
+ return {"success": False, "error": "Bawal sa bata: unsafe description detected"}
228
+
229
+ formatted_story = generate_story_openai(description, audience)
230
+ story_category = "OPENAI"
231
+
232
+ return {
233
+ "success": True,
234
+ "description": description,
235
+ "category": story_category,
236
+ "confidence": None,
237
+ "probabilities": probs,
238
+ "story": formatted_story
239
+ }
240
+
241
+
242
+
243
+ # GRADIO FUNCTION
244
+ def generate_story_gradio(image, audience):
245
+ buffered = io.BytesIO()
246
+ image.save(buffered, format="PNG")
247
+ img_b64 = base64.b64encode(buffered.getvalue()).decode()
248
+ result = process_image_base64(img_b64, audience)
249
+ if not result["success"]:
250
+ return result.get("error", "Failed"), "", "", ""
251
+ # Story is already formatted as a string
252
+ return result["description"], result["category"], str(result["probabilities"]), result["story"]
253
+
254
+ # GRADIO UI
255
+ with gr.Blocks(title=" Tree & House Story Generator") as demo:
256
+ gr.Markdown("# Tree & House Story Generator")
257
+ gr.Markdown("Upload a drawing and generate a creative story!")
258
+
259
+ with gr.Row():
260
+ with gr.Column():
261
+ image_input = gr.Image(type="pil", label="Upload a drawing")
262
+ audience_input = gr.Dropdown(["Children", "Teens"], value="Children", label="Audience")
263
+ btn = gr.Button(" Generate Story", variant="primary")
264
+
265
+ with gr.Column():
266
+ description_output = gr.Textbox(label="Image Description", lines=2)
267
+ category_output = gr.Textbox(label="Category")
268
+ probabilities_output = gr.Textbox(label="Probabilities")
269
+ story_output = gr.Textbox(label="Story", lines=12)
270
+
271
+ btn.click(
272
+ generate_story_gradio,
273
+ inputs=[image_input, audience_input],
274
+ outputs=[description_output, category_output, probabilities_output, story_output]
275
+ )
276
+
277
+ # FLASK API
278
+ app = Flask(__name__)
279
+ CORS(app)
280
+
281
+ # Create Gradio WSGI app with SSR disabled
282
+ gradio_app = gr.routes.App.create_app(demo, ssr_mode=False)
283
+
284
+ @app.route('/')
285
+ def index():
286
+ return Response("Redirecting to Gradio UI...", status=302, headers={"Location": "/gradio"})
287
+
288
+ @app.route("/api/health", methods=["GET"])
289
+ def health_check():
290
+ return jsonify({
291
+ "status": "healthy",
292
+ "model_loaded": classifier is not None,
293
+ "tree_stories": len(tree_stories),
294
+ "house_stories": len(house_stories)
295
+ })
296
+
297
+ @app.route("/api/generate-story-base64", methods=["POST"])
298
+ def generate_story_api():
299
+ data = request.get_json()
300
+ if "image" not in data:
301
+ return jsonify({"error": "No image provided"}), 400
302
+ audience = data.get("audience", "Children")
303
+ result = process_image_base64(data["image"], audience)
304
+ return jsonify(result)
305
+
306
+ @app.route('/api/test', methods=['GET'])
307
+ def api_test():
308
+ """API documentation and test page"""
309
+ html = """
310
+ <!DOCTYPE html>
311
+ <html>
312
+ <head>
313
+ <title>API Test - Story Generator</title>
314
+ <style>
315
+ body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
316
+ h1 { color: #333; }
317
+ .endpoint { background: #f5f5f5; padding: 15px; margin: 20px 0; border-radius: 5px; }
318
+ .method { display: inline-block; padding: 5px 10px; border-radius: 3px; font-weight: bold; }
319
+ .get { background: #61affe; color: white; }
320
+ .post { background: #49cc90; color: white; }
321
+ code { background: #f5f5f5; padding: 2px 5px; border-radius: 3px; }
322
+ .test-section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
323
+ button { background: #49cc90; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 5px; }
324
+ button:hover { background: #3da877; }
325
+ #result { margin-top: 15px; padding: 15px; background: #f9f9f9; border-radius: 5px; white-space: pre-wrap; }
326
+ </style>
327
+ </head>
328
+ <body>
329
+ <h1> Story Generator API</h1>
330
+
331
+ <div class="endpoint">
332
+ <span class="method get">GET</span> <code>/api/health</code>
333
+ <p>Check API status</p>
334
+ </div>
335
+
336
+ <div class="endpoint">
337
+ <span class="method post">POST</span> <code>/api/generate-story-base64</code>
338
+ <p>Generate story from base64 image</p>
339
+ <strong>Request body:</strong>
340
+ <pre>{
341
+ "image": "base64_encoded_image_string",
342
+ "audience": "Children"
343
+ }</pre>
344
+ </div>
345
+
346
+ <div class="test-section">
347
+ <h2>Test API</h2>
348
+ <p>Upload an image to test the story generation:</p>
349
+ <input type="file" id="imageInput" accept="image/*">
350
+ <br><br>
351
+ <label>Audience:
352
+ <select id="audience">
353
+ <option>Children</option>
354
+ <option>Teens</option>
355
+ </select>
356
+ </label>
357
+ <br><br>
358
+ <button onclick="testAPI()">Generate Story</button>
359
+ <div id="result"></div>
360
+ </div>
361
+
362
+ <script>
363
+ async function testAPI() {
364
+ const fileInput = document.getElementById('imageInput');
365
+ const audience = document.getElementById('audience').value;
366
+ const resultDiv = document.getElementById('result');
367
+
368
+ if (!fileInput.files[0]) {
369
+ resultDiv.textContent = 'Please select an image first!';
370
+ return;
371
+ }
372
+
373
+ resultDiv.textContent = 'Processing...';
374
+
375
+ const reader = new FileReader();
376
+ reader.onload = async function(e) {
377
+ const base64 = e.target.result.split(',')[1];
378
+
379
+ try {
380
+ const response = await fetch('/api/generate-story-base64', {
381
+ method: 'POST',
382
+ headers: {
383
+ 'Content-Type': 'application/json'
384
+ },
385
+ body: JSON.stringify({
386
+ image: base64,
387
+ audience: audience
388
+ })
389
+ });
390
+
391
+ const data = await response.json();
392
+ resultDiv.textContent = JSON.stringify(data, null, 2);
393
+ } catch (error) {
394
+ resultDiv.textContent = 'Error: ' + error.message;
395
+ }
396
+ };
397
+
398
+ reader.readAsDataURL(fileInput.files[0]);
399
+ }
400
+ </script>
401
+ </body>
402
+ </html>
403
+ """
404
+ return html
405
+
406
+ # Mount Gradio at /gradio using WSGI middleware
407
+ from werkzeug.middleware.dispatcher import DispatcherMiddleware
408
+
409
+ app.wsgi_app = DispatcherMiddleware(
410
+ app.wsgi_app,
411
+ {'/gradio': gradio_app}
412
+ )
413
+
414
+ # RUN FLASK APP
415
+ if __name__ == "__main__":
416
+ port = int(os.environ.get("PORT", 7860))
417
+ print("=" * 60)
418
+ print(" Flask + Gradio Server Starting")
419
+ print("=" * 60)
420
+ print(f" Gradio UI: http://localhost:{port}/gradio")
421
+ print(f" Home (Redirect): http://localhost:{port}/")
422
+ print(f" API Test Page: http://localhost:{port}/api/test")
423
+ print(f" Health Check: http://localhost:{port}/api/health")
424
+ print(f" API Endpoint: http://localhost:{port}/api/generate-story-base64")
425
+ print("=" * 60)
426
+ app.run(host="0.0.0.0", port=port, debug=False)