andevs commited on
Commit
458f5ef
·
verified ·
1 Parent(s): 5f1fd90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +480 -93
app.py CHANGED
@@ -1,18 +1,26 @@
1
  """
2
- StudyFlow Backend - Lightweight API for Hugging Face
 
3
  """
4
 
5
- from fastapi import FastAPI, File, UploadFile, Form
6
  from fastapi.middleware.cors import CORSMiddleware
7
- from fastapi.responses import JSONResponse
8
  from datetime import datetime
9
  import json
10
  import PyPDF2
11
  import io
 
 
 
12
 
13
- app = FastAPI(title="StudyFlow API")
 
 
 
 
14
 
15
- # CORS
16
  app.add_middleware(
17
  CORSMiddleware,
18
  allow_origins=["*"],
@@ -21,128 +29,507 @@ app.add_middleware(
21
  allow_headers=["*"],
22
  )
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  @app.get("/")
25
  async def root():
26
- return {"service": "StudyFlow API", "status": "healthy"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  @app.get("/health")
29
  async def health():
30
- return {"status": "healthy", "timestamp": datetime.now().isoformat()}
 
 
 
 
 
31
 
32
  @app.post("/api/generate-questions")
33
  async def generate_questions(
34
  text: str = Form(...),
35
  difficulty: str = Form("medium"),
36
- mode: str = Form("mixed")
 
37
  ):
38
- """Generate questions based on text and user preferences"""
39
-
40
- # Simple question generation logic
41
- words = text.split()
42
- if len(words) < 50:
43
- return {"error": "Text too short. Please provide more content."}
44
-
45
- # Extract key terms (simple implementation)
46
- key_terms = [word for word in words if len(word) > 5][:5]
47
 
48
- # Generate questions based on mode
49
- questions = []
50
-
51
- if mode in ["practice", "mixed"]:
52
- questions.extend([
53
- {
54
- "id": "q1",
55
- "type": "multiple_choice",
56
- "question": "What is the main topic discussed?",
57
- "options": [
58
- "General introduction and overview",
59
- "Technical specifications",
60
- "Historical context",
61
- "Future applications"
62
- ],
63
- "correct_answer": 0,
64
- "explanation": "The text provides a foundational overview of the subject.",
65
- "difficulty": difficulty,
66
- "concept": "Main Topic"
67
- }
68
- ])
69
-
70
- if mode in ["theory", "mixed"]:
71
- questions.append({
72
- "id": "q2",
73
- "type": "short_answer",
74
- "question": "Summarize the key point in one sentence.",
75
- "correct_answer": "The material introduces important concepts for understanding the subject.",
76
- "explanation": "This captures the essential purpose of the text.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  "difficulty": difficulty,
78
- "concept": "Summary"
79
- })
80
-
81
- return {
82
- "questions": questions,
83
- "concepts": ["Main Topic", "Key Concepts"],
84
- "generated_at": datetime.now().isoformat()
85
- }
 
 
 
86
 
87
  @app.post("/api/generate-flashcards")
88
- async def generate_flashcards(text: str = Form(...)):
89
- """Generate flashcards from text"""
90
-
91
- # Simple flashcard generation
92
- words = text.split()
93
- key_terms = [word for word in words if word[0].isupper() and len(word) > 3][:3]
94
-
95
- flashcards = []
96
- for i, term in enumerate(key_terms, 1):
97
- flashcards.append({
98
- "id": f"f{i}",
99
- "front": f"What is {term}?",
100
- "back": f"A key concept mentioned in the text",
101
- "category": "Terminology"
102
- })
103
 
104
- return {
105
- "flashcards": flashcards,
106
- "generated_at": datetime.now().isoformat()
107
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  @app.post("/api/summarize")
110
- async def summarize(text: str = Form(...)):
111
- """Generate summary of text"""
 
 
 
 
112
 
113
- # Simple summarization (first 200 chars)
114
- summary = text[:200] + ("..." if len(text) > 200 else "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- return {
117
- "summary": summary,
118
- "length": len(text),
119
- "generated_at": datetime.now().isoformat()
120
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  @app.post("/api/upload-pdf")
123
  async def upload_pdf(file: UploadFile = File(...)):
124
- """Extract text from PDF"""
 
 
 
 
 
125
  try:
 
 
 
 
 
 
 
 
126
  contents = await file.read()
127
- pdf_reader = PyPDF2.PdfReader(io.BytesIO(contents))
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  text = ""
130
- for page in pdf_reader.pages[:5]: # Limit to first 5 pages
131
- text += page.extract_text() + "\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  return {
 
 
134
  "filename": file.filename,
135
- "text_length": len(text),
136
- "text_preview": text[:500],
137
- "pages": len(pdf_reader.pages)
 
 
 
 
138
  }
139
 
 
 
140
  except Exception as e:
141
- return JSONResponse(
142
- status_code=400,
143
- content={"error": f"Failed to process PDF: {str(e)}"}
144
  )
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  if __name__ == "__main__":
147
  import uvicorn
148
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
1
  """
2
+ StudyFlow Backend API for Hugging Face Spaces
3
+ Complete implementation with PDF processing and content generation
4
  """
5
 
6
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse, HTMLResponse
9
  from datetime import datetime
10
  import json
11
  import PyPDF2
12
  import io
13
+ import re
14
+ from typing import List, Dict, Any
15
+ import os
16
 
17
+ app = FastAPI(
18
+ title="StudyFlow API",
19
+ description="AI-powered learning assistant backend",
20
+ version="1.0.0"
21
+ )
22
 
23
+ # Enable CORS for frontend
24
  app.add_middleware(
25
  CORSMiddleware,
26
  allow_origins=["*"],
 
29
  allow_headers=["*"],
30
  )
31
 
32
+ # Store for user sessions (in production, use database)
33
+ user_sessions = {}
34
+
35
+ def extract_key_concepts(text: str, max_concepts: int = 5) -> List[str]:
36
+ """Extract key concepts from text using simple NLP techniques"""
37
+ words = re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', text)
38
+ word_freq = {}
39
+
40
+ for word in words:
41
+ if len(word) > 3: # Ignore short words
42
+ word_freq[word] = word_freq.get(word, 0) + 1
43
+
44
+ # Sort by frequency and get top concepts
45
+ sorted_concepts = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
46
+ return [concept for concept, _ in sorted_concepts[:max_concepts]]
47
+
48
+ def generate_question_from_concept(concept: str, difficulty: str, question_type: str = "multiple_choice") -> Dict[str, Any]:
49
+ """Generate a question based on a concept"""
50
+ question_id = f"q_{datetime.now().timestamp()}"
51
+
52
+ if question_type == "multiple_choice":
53
+ return {
54
+ "id": question_id,
55
+ "type": "multiple_choice",
56
+ "question": f"What is the significance of {concept}?",
57
+ "options": [
58
+ f"{concept} is a fundamental concept in this field",
59
+ f"{concept} represents a technical specification",
60
+ f"{concept} is a historical figure",
61
+ f"{concept} is a recent development"
62
+ ],
63
+ "correct_answer": 0,
64
+ "explanation": f"{concept} is mentioned throughout the text as a key concept.",
65
+ "difficulty": difficulty,
66
+ "concept": concept
67
+ }
68
+ else: # short_answer
69
+ return {
70
+ "id": question_id,
71
+ "type": "short_answer",
72
+ "question": f"Explain {concept} in your own words.",
73
+ "correct_answer": f"{concept} is an important concept discussed in the material.",
74
+ "explanation": f"This requires understanding of {concept} from the context.",
75
+ "difficulty": difficulty,
76
+ "concept": concept
77
+ }
78
+
79
+ def generate_flashcard_from_concept(concept: str) -> Dict[str, Any]:
80
+ """Generate a flashcard from a concept"""
81
+ return {
82
+ "id": f"f_{datetime.now().timestamp()}",
83
+ "front": f"What is {concept}?",
84
+ "back": f"{concept} is a key concept discussed in the material.",
85
+ "category": "Key Concepts",
86
+ "difficulty": "medium",
87
+ "lastReviewed": None,
88
+ "nextReview": None,
89
+ "easeFactor": 2.5
90
+ }
91
+
92
  @app.get("/")
93
  async def root():
94
+ """Root endpoint with API information"""
95
+ return {
96
+ "service": "StudyFlow API",
97
+ "version": "1.0.0",
98
+ "status": "running",
99
+ "endpoints": {
100
+ "generate_questions": "POST /api/generate-questions",
101
+ "generate_flashcards": "POST /api/generate-flashcards",
102
+ "summarize": "POST /api/summarize",
103
+ "explain": "POST /api/explain",
104
+ "upload_pdf": "POST /api/upload-pdf",
105
+ "health": "GET /health"
106
+ },
107
+ "documentation": "/docs"
108
+ }
109
 
110
  @app.get("/health")
111
  async def health():
112
+ """Health check endpoint"""
113
+ return {
114
+ "status": "healthy",
115
+ "timestamp": datetime.now().isoformat(),
116
+ "sessions": len(user_sessions)
117
+ }
118
 
119
  @app.post("/api/generate-questions")
120
  async def generate_questions(
121
  text: str = Form(...),
122
  difficulty: str = Form("medium"),
123
+ mode: str = Form("mixed"),
124
+ num_questions: int = Form(5)
125
  ):
126
+ """
127
+ Generate educational questions from text
 
 
 
 
 
 
 
128
 
129
+ Args:
130
+ text: Input text to analyze
131
+ difficulty: Difficulty level (easy, medium, hard)
132
+ mode: Question type (theory, practice, mixed, flashcards)
133
+ num_questions: Number of questions to generate
134
+ """
135
+ try:
136
+ if not text or len(text.strip()) < 100:
137
+ raise HTTPException(
138
+ status_code=400,
139
+ detail="Text too short. Please provide at least 100 characters."
140
+ )
141
+
142
+ # Validate inputs
143
+ if difficulty not in ["easy", "medium", "hard"]:
144
+ difficulty = "medium"
145
+
146
+ if mode not in ["theory", "practice", "mixed", "flashcards"]:
147
+ mode = "mixed"
148
+
149
+ num_questions = max(1, min(num_questions, 20)) # Limit to 1-20 questions
150
+
151
+ # Extract key concepts from text
152
+ key_concepts = extract_key_concepts(text, max_concepts=min(num_questions, 10))
153
+
154
+ # Generate questions based on mode
155
+ questions = []
156
+ concept_index = 0
157
+
158
+ while len(questions) < num_questions and concept_index < len(key_concepts):
159
+ if mode == "theory" or mode == "mixed":
160
+ # Generate theory questions (short answer)
161
+ questions.append(generate_question_from_concept(
162
+ key_concepts[concept_index],
163
+ difficulty,
164
+ "short_answer"
165
+ ))
166
+ concept_index += 1
167
+
168
+ if len(questions) >= num_questions:
169
+ break
170
+
171
+ if mode == "practice" or mode == "mixed":
172
+ # Generate practice questions (multiple choice)
173
+ questions.append(generate_question_from_concept(
174
+ key_concepts[concept_index],
175
+ difficulty,
176
+ "multiple_choice"
177
+ ))
178
+ concept_index += 1
179
+
180
+ if len(questions) >= num_questions:
181
+ break
182
+
183
+ # If we don't have enough concepts, add generic questions
184
+ while len(questions) < num_questions:
185
+ generic_concept = "the main topic" if len(questions) % 2 == 0 else "key concepts"
186
+ questions.append(generate_question_from_concept(
187
+ generic_concept,
188
+ difficulty,
189
+ "multiple_choice" if len(questions) % 2 == 0 else "short_answer"
190
+ ))
191
+
192
+ return {
193
+ "success": True,
194
+ "questions": questions[:num_questions],
195
+ "concepts": key_concepts,
196
+ "count": len(questions[:num_questions]),
197
  "difficulty": difficulty,
198
+ "mode": mode,
199
+ "generated_at": datetime.now().isoformat()
200
+ }
201
+
202
+ except HTTPException:
203
+ raise
204
+ except Exception as e:
205
+ raise HTTPException(
206
+ status_code=500,
207
+ detail=f"Failed to generate questions: {str(e)}"
208
+ )
209
 
210
  @app.post("/api/generate-flashcards")
211
+ async def generate_flashcards(
212
+ text: str = Form(...),
213
+ difficulty: str = Form("medium")
214
+ ):
215
+ """
216
+ Generate flashcards from text
 
 
 
 
 
 
 
 
 
217
 
218
+ Args:
219
+ text: Input text to analyze
220
+ difficulty: Difficulty level for flashcards
221
+ """
222
+ try:
223
+ if not text or len(text.strip()) < 100:
224
+ raise HTTPException(
225
+ status_code=400,
226
+ detail="Text too short. Please provide at least 100 characters."
227
+ )
228
+
229
+ # Extract key concepts
230
+ key_concepts = extract_key_concepts(text, max_concepts=10)
231
+
232
+ # Generate flashcards
233
+ flashcards = []
234
+ for i, concept in enumerate(key_concepts):
235
+ if i >= 10: # Limit to 10 flashcards
236
+ break
237
+
238
+ flashcard = generate_flashcard_from_concept(concept)
239
+
240
+ # Adjust based on difficulty
241
+ if difficulty == "easy":
242
+ flashcard["difficulty"] = "easy"
243
+ flashcard["back"] = f"{concept} is a basic concept mentioned in the text."
244
+ elif difficulty == "hard":
245
+ flashcard["difficulty"] = "hard"
246
+ flashcard["back"] = f"{concept} represents a complex concept requiring deeper understanding."
247
+
248
+ flashcards.append(flashcard)
249
+
250
+ # Add some generic flashcards if we don't have enough concepts
251
+ generic_flashcards = [
252
+ {
253
+ "id": f"f_gen_{i}",
254
+ "front": "What is the main topic of this material?",
255
+ "back": "The text discusses key concepts and principles.",
256
+ "category": "Overview",
257
+ "difficulty": difficulty,
258
+ "lastReviewed": None,
259
+ "nextReview": None,
260
+ "easeFactor": 2.5
261
+ }
262
+ for i in range(max(0, 5 - len(flashcards)))
263
+ ]
264
+
265
+ flashcards.extend(generic_flashcards)
266
+
267
+ return {
268
+ "success": True,
269
+ "flashcards": flashcards,
270
+ "count": len(flashcards),
271
+ "generated_at": datetime.now().isoformat()
272
+ }
273
+
274
+ except HTTPException:
275
+ raise
276
+ except Exception as e:
277
+ raise HTTPException(
278
+ status_code=500,
279
+ detail=f"Failed to generate flashcards: {str(e)}"
280
+ )
281
 
282
  @app.post("/api/summarize")
283
+ async def summarize(
284
+ text: str = Form(...),
285
+ difficulty: str = Form("medium")
286
+ ):
287
+ """
288
+ Generate a summary of the text
289
 
290
+ Args:
291
+ text: Text to summarize
292
+ difficulty: Reading level for summary
293
+ """
294
+ try:
295
+ if not text:
296
+ raise HTTPException(
297
+ status_code=400,
298
+ detail="No text provided."
299
+ )
300
+
301
+ # Simple summarization - first 200 words
302
+ words = text.split()
303
+ word_count = len(words)
304
+
305
+ if word_count < 100:
306
+ summary = text
307
+ else:
308
+ # Take first 10% of text for summary
309
+ summary_length = max(50, min(500, word_count // 10))
310
+ summary_words = words[:summary_length]
311
+ summary = ' '.join(summary_words)
312
+ if word_count > summary_length:
313
+ summary += "..."
314
+
315
+ # Adjust summary based on difficulty
316
+ if difficulty == "easy":
317
+ intro = "This is a simple summary:\n\n"
318
+ elif difficulty == "hard":
319
+ intro = "Detailed analysis:\n\n"
320
+ else:
321
+ intro = "Summary:\n\n"
322
+
323
+ # Add metadata
324
+ reading_time = max(1, word_count // 200) # 200 words per minute
325
+
326
+ full_summary = f"""{intro}{summary}
327
+
328
+ Key Information:
329
+ • Word count: {word_count}
330
+ • Estimated reading time: {reading_time} minute{'s' if reading_time != 1 else ''}
331
+ • Key concepts: {', '.join(extract_key_concepts(text, 3))}
332
+
333
+ Study recommendations:
334
+ 1. Read through the summary first
335
+ 2. Identify unfamiliar terms
336
+ 3. Create flashcards for key concepts
337
+ 4. Test your understanding with practice questions"""
338
+
339
+ return {
340
+ "success": True,
341
+ "summary": full_summary,
342
+ "word_count": word_count,
343
+ "reading_time": reading_time,
344
+ "generated_at": datetime.now().isoformat()
345
+ }
346
+
347
+ except HTTPException:
348
+ raise
349
+ except Exception as e:
350
+ raise HTTPException(
351
+ status_code=500,
352
+ detail=f"Failed to generate summary: {str(e)}"
353
+ )
354
+
355
+ @app.post("/api/explain")
356
+ async def explain(
357
+ topic: str = Form(...),
358
+ context: str = Form("")
359
+ ):
360
+ """
361
+ Explain a specific topic
362
 
363
+ Args:
364
+ topic: Topic to explain
365
+ context: Context from the material
366
+ """
367
+ try:
368
+ if not topic:
369
+ raise HTTPException(
370
+ status_code=400,
371
+ detail="No topic provided."
372
+ )
373
+
374
+ # Generate explanation based on topic
375
+ if context and len(context) > 100:
376
+ # If we have context, create a more specific explanation
377
+ explanation = f"""**Explanation of {topic}:**
378
+
379
+ Based on the provided material, {topic} appears to be an important concept.
380
+
381
+ The text mentions this in the context of: "{context[:200]}..."
382
+
383
+ Key points about {topic}:
384
+ • Likely relates to the main themes discussed
385
+ • May be connected to other concepts in the text
386
+ • Understanding this is important for grasping the material
387
+
388
+ To better understand {topic}, try:
389
+ 1. Looking for definitions in the text
390
+ 2. Finding examples of how it's used
391
+ 3. Creating a flashcard to review later"""
392
+ else:
393
+ # Generic explanation
394
+ explanation = f"""**Explanation of {topic}:**
395
+
396
+ {topic} is a concept that requires understanding. In learning materials, concepts like this typically:
397
+
398
+ • Represent important ideas or principles
399
+ • Connect to other topics in meaningful ways
400
+ • Have practical applications or implications
401
+
402
+ To master {topic}:
403
+ 1. Read about it in multiple sources
404
+ 2. Try explaining it in your own words
405
+ 3. Create practice questions about it
406
+ 4. Review it regularly using spaced repetition"""
407
+
408
+ return {
409
+ "success": True,
410
+ "topic": topic,
411
+ "explanation": explanation,
412
+ "generated_at": datetime.now().isoformat()
413
+ }
414
+
415
+ except HTTPException:
416
+ raise
417
+ except Exception as e:
418
+ raise HTTPException(
419
+ status_code=500,
420
+ detail=f"Failed to generate explanation: {str(e)}"
421
+ )
422
 
423
  @app.post("/api/upload-pdf")
424
  async def upload_pdf(file: UploadFile = File(...)):
425
+ """
426
+ Extract text from uploaded PDF
427
+
428
+ Args:
429
+ file: PDF file to process
430
+ """
431
  try:
432
+ # Check file type
433
+ if not file.filename.lower().endswith('.pdf'):
434
+ raise HTTPException(
435
+ status_code=400,
436
+ detail="Only PDF files are supported."
437
+ )
438
+
439
+ # Read file content
440
  contents = await file.read()
 
441
 
442
+ if len(contents) > 10 * 1024 * 1024: # 10MB limit
443
+ raise HTTPException(
444
+ status_code=400,
445
+ detail="File too large. Maximum size is 10MB."
446
+ )
447
+
448
+ # Extract text from PDF
449
+ pdf_file = io.BytesIO(contents)
450
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
451
+
452
+ if len(pdf_reader.pages) == 0:
453
+ raise HTTPException(
454
+ status_code=400,
455
+ detail="PDF file appears to be empty or corrupted."
456
+ )
457
+
458
+ # Extract text from pages (limit to first 10 pages for performance)
459
  text = ""
460
+ pages_to_extract = min(10, len(pdf_reader.pages))
461
+
462
+ for i in range(pages_to_extract):
463
+ try:
464
+ page_text = pdf_reader.pages[i].extract_text()
465
+ if page_text:
466
+ text += page_text + "\n\n"
467
+ except Exception as e:
468
+ print(f"Error extracting page {i+1}: {e}")
469
+ continue
470
+
471
+ if not text.strip():
472
+ raise HTTPException(
473
+ status_code=400,
474
+ detail="Could not extract text from PDF. The file might be scanned or image-based."
475
+ )
476
+
477
+ # Extract metadata
478
+ word_count = len(text.split())
479
+
480
+ # Store session
481
+ session_id = f"session_{datetime.now().timestamp()}"
482
+ user_sessions[session_id] = {
483
+ "filename": file.filename,
484
+ "size": len(contents),
485
+ "pages": len(pdf_reader.pages),
486
+ "extracted_pages": pages_to_extract,
487
+ "word_count": word_count,
488
+ "uploaded_at": datetime.now().isoformat()
489
+ }
490
 
491
  return {
492
+ "success": True,
493
+ "session_id": session_id,
494
  "filename": file.filename,
495
+ "text": text,
496
+ "text_preview": text[:1000],
497
+ "pages": len(pdf_reader.pages),
498
+ "extracted_pages": pages_to_extract,
499
+ "word_count": word_count,
500
+ "file_size": len(contents),
501
+ "uploaded_at": datetime.now().isoformat()
502
  }
503
 
504
+ except HTTPException:
505
+ raise
506
  except Exception as e:
507
+ raise HTTPException(
508
+ status_code=500,
509
+ detail=f"Failed to process PDF: {str(e)}"
510
  )
511
 
512
+ @app.get("/api/session/{session_id}")
513
+ async def get_session(session_id: str):
514
+ """Get information about a session"""
515
+ if session_id not in user_sessions:
516
+ raise HTTPException(status_code=404, detail="Session not found")
517
+
518
+ return {
519
+ "success": True,
520
+ "session": user_sessions[session_id]
521
+ }
522
+
523
+ @app.delete("/api/session/{session_id}")
524
+ async def delete_session(session_id: str):
525
+ """Delete a session"""
526
+ if session_id in user_sessions:
527
+ del user_sessions[session_id]
528
+ return {"success": True, "message": "Session deleted"}
529
+ else:
530
+ raise HTTPException(status_code=404, detail="Session not found")
531
+
532
  if __name__ == "__main__":
533
  import uvicorn
534
+ port = int(os.getenv("PORT", 7860))
535
+ uvicorn.run(app, host="0.0.0.0", port=port)