geethareddy commited on
Commit
c53f251
·
verified ·
1 Parent(s): 741c6ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +274 -186
app.py CHANGED
@@ -1,29 +1,205 @@
1
- from flask import Flask, render_template_string, request, jsonify
2
  import speech_recognition as sr
 
 
3
  from tempfile import NamedTemporaryFile
4
- import os
5
  import ffmpeg
6
- import logging
7
- from werkzeug.exceptions import BadRequest
8
 
 
9
  app = Flask(__name__)
10
  logging.basicConfig(level=logging.INFO)
11
 
12
  # Global variables
13
- cart = [] # To store items, quantities, and prices
14
- MENU = {
15
- "Biryani": {"Chicken Biryani": 250, "Veg Biryani": 200, "Mutton Biryani": 300},
16
- "Starters": {"Chicken Wings": 220, "Paneer Tikka": 180, "Fish Fingers": 250, "Spring Rolls": 160},
17
- "Breads": {"Butter Naan": 50, "Garlic Naan": 60, "Roti": 40, "Lachha Paratha": 70},
18
- "Curries": {"Butter Chicken": 300, "Paneer Butter Masala": 250, "Dal Tadka": 200, "Chicken Tikka Masala": 320},
19
- "Drinks": {"Coke": 60, "Sprite": 60, "Mango Lassi": 80, "Masala Soda": 70},
20
- "Desserts": {"Gulab Jamun": 100, "Rasgulla": 90, "Ice Cream": 120, "Brownie with Ice Cream": 180},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
- current_category = None
23
- current_item = None
24
- awaiting_quantity = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- # HTML Template for Frontend
27
  html_code = """
28
  <!DOCTYPE html>
29
  <html lang="en">
@@ -33,25 +209,43 @@ html_code = """
33
  <title>AI Dining Assistant</title>
34
  <style>
35
  body {
 
 
 
 
 
 
36
  font-family: Arial, sans-serif;
37
- text-align: center;
38
  background-color: #f4f4f9;
39
  }
40
  h1 {
41
  color: #333;
42
  }
43
  .mic-button {
44
- width: 80px;
45
- height: 80px;
46
- border-radius: 50%;
47
- background-color: #007bff;
48
  color: white;
49
- font-size: 24px;
50
  border: none;
 
51
  cursor: pointer;
 
 
 
 
52
  }
53
  .status, .response {
54
- margin-top: 20px;
 
 
 
 
 
 
 
 
 
 
55
  }
56
  </style>
57
  </head>
@@ -59,186 +253,80 @@ html_code = """
59
  <h1>AI Dining Assistant</h1>
60
  <button class="mic-button" id="mic-button">🎤</button>
61
  <div class="status" id="status">Press the mic button to start...</div>
62
- <div class="response" id="response" style="display: none;">Response will appear here...</div>
63
  <script>
64
  const micButton = document.getElementById('mic-button');
65
  const status = document.getElementById('status');
66
  const response = document.getElementById('response');
67
- let isListening = false;
68
-
 
69
  micButton.addEventListener('click', () => {
70
- if (!isListening) {
71
- isListening = true;
72
- greetUser();
73
  }
74
  });
75
-
76
- function greetUser() {
77
- const utterance = new SpeechSynthesisUtterance("Hi. Welcome to Biryani Hub. Can I show you the menu?");
78
  speechSynthesis.speak(utterance);
79
  utterance.onend = () => {
80
- status.textContent = "Listening...";
81
  startListening();
82
  };
83
  }
84
-
85
- async function startListening() {
86
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
87
- const mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
88
- const audioChunks = [];
89
-
90
- mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
91
- mediaRecorder.onstop = async () => {
92
- const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
93
- const formData = new FormData();
94
- formData.append("audio", audioBlob);
95
-
96
- status.textContent = "Processing...";
97
- try {
98
- const result = await fetch("/process-audio", { method: "POST", body: formData });
99
- const data = await result.json();
100
- response.textContent = data.response;
101
- response.style.display = "block";
102
-
103
- const utterance = new SpeechSynthesisUtterance(data.response);
104
- speechSynthesis.speak(utterance);
105
- utterance.onend = () => {
106
- if (!data.response.includes("Goodbye") && !data.response.includes("final order")) {
107
- startListening(); // Continue listening
108
- } else {
109
- status.textContent = "Conversation ended.";
110
- isListening = false;
111
- }
112
- };
113
- } catch (error) {
114
- response.textContent = "Error processing your request. Please try again.";
115
- status.textContent = "Press the mic button to restart.";
116
- isListening = false;
117
- }
118
- };
119
-
120
- mediaRecorder.start();
121
- setTimeout(() => mediaRecorder.stop(), 5000); // Stop recording after 5 seconds
 
 
 
 
 
 
 
 
 
122
  }
123
  </script>
124
  </body>
125
  </html>
126
  """
127
 
128
- @app.route("/")
129
- def index():
130
- return render_template_string(html_code)
131
-
132
- @app.route("/process-audio", methods=["POST"])
133
- def process_audio():
134
- global current_category, current_item, awaiting_quantity
135
- try:
136
- audio_file = request.files.get("audio")
137
- if not audio_file:
138
- raise BadRequest("No audio file provided.")
139
-
140
- temp_file = NamedTemporaryFile(delete=False, suffix=".webm")
141
- audio_file.save(temp_file.name)
142
-
143
- if os.path.getsize(temp_file.name) == 0:
144
- raise BadRequest("Uploaded audio file is empty.")
145
-
146
- converted_file = NamedTemporaryFile(delete=False, suffix=".wav")
147
- ffmpeg.input(temp_file.name).output(
148
- converted_file.name, acodec="pcm_s16le", ac=1, ar="16000"
149
- ).run(overwrite_output=True)
150
-
151
- recognizer = sr.Recognizer()
152
- with sr.AudioFile(converted_file.name) as source:
153
- audio_data = recognizer.record(source)
154
- try:
155
- command = recognizer.recognize_google(audio_data)
156
- logging.info(f"Recognized command: {command}")
157
- response = process_command(command)
158
- except sr.UnknownValueError:
159
- response = "Sorry, I couldn't understand your command. Could you please repeat?"
160
- except sr.RequestError as e:
161
- response = f"Error with the speech recognition service: {e}"
162
-
163
- return jsonify({"response": response})
164
-
165
- except BadRequest as br:
166
- return jsonify({"response": f"Bad Request: {str(br)}"}), 400
167
- except Exception as e:
168
- return jsonify({"response": f"An error occurred: {str(e)}"}), 500
169
- finally:
170
- os.unlink(temp_file.name)
171
- os.unlink(converted_file.name)
172
-
173
- def process_command(command):
174
- global cart, MENU, current_category, current_item, awaiting_quantity
175
- command = command.lower()
176
-
177
- # Handle quantity input
178
- if awaiting_quantity and command.isdigit():
179
- quantity = int(command)
180
- if quantity > 0:
181
- cart.append((current_item, MENU[current_category][current_item], quantity))
182
- awaiting_quantity = False
183
- item = current_item
184
- current_item = None
185
- total = sum(i[1] * i[2] for i in cart)
186
- cart_summary = ", ".join([f"{i[0]} x{i[2]} (₹{i[1] * i[2]})" for i in cart])
187
- return f"Added {quantity} of {item} to your cart. Cart: {cart_summary}. Total: ₹{total}. Would you like to see the menu again?"
188
- else:
189
- return "Quantity must be at least 1. How many would you like to order?"
190
-
191
- # Handle category selection
192
- for category, items in MENU.items():
193
- if category.lower() in command:
194
- current_category = category
195
- item_list = ", ".join([f"{item} (₹{price})" for item, price in items.items()])
196
- return f"{category} menu: {item_list}. What would you like to order?"
197
-
198
- # Handle item selection with dynamic matching
199
- if current_category:
200
- for item in MENU[current_category].keys():
201
- if item.lower().startswith(command) or command in item.lower():
202
- current_item = item
203
- awaiting_quantity = True
204
- return f"How many quantities of {current_item} would you like?"
205
-
206
- # Handle item removal
207
- if "remove" in command:
208
- for item in cart:
209
- if item[0].lower() in command:
210
- cart.remove(item)
211
- total = sum(i[1] * i[2] for i in cart)
212
- cart_summary = ", ".join([f"{i[0]} x{i[2]} (₹{i[1] * i[2]})" for i in cart])
213
- return f"Removed {item[0]} from your cart. Updated cart: {cart_summary}. Total: ₹{total}."
214
- return "The item you are trying to remove is not in your cart."
215
-
216
- # Handle final order
217
- if "final order" in command or "submit" in command:
218
- if cart:
219
- order_details = ", ".join([f"{item[0]} x{item[2]} (₹{item[1] * item[2]})" for item in cart])
220
- total = sum(item[1] * item[2] for item in cart)
221
- cart.clear()
222
- return f"Your final order is: {order_details}. Total price: ₹{total}. Thank you for visiting Biryani Hub!"
223
- else:
224
- return "Your cart is empty. Please add items before placing the final order."
225
-
226
- # Handle cart details
227
- if "cart details" in command:
228
- if cart:
229
- cart_summary = "\n".join([f"{i[0]} x{i[2]} (₹{i[1] * i[2]})" for i in cart])
230
- total = sum(i[1] * i[2] for i in cart)
231
- return f"Your cart contains:\n{cart_summary}\nTotal: ₹{total}."
232
- else:
233
- return "Your cart is empty."
234
-
235
- # Generic response for menu request
236
- if "menu" in command:
237
- categories = ", ".join(MENU.keys())
238
- return f"We have the following categories: {categories}. Please select a category to proceed."
239
-
240
- # Default response
241
- return "Sorry, I didn't understand that. Please try again."
242
-
243
  if __name__ == "__main__":
244
  app.run(host="0.0.0.0", port=7860)
 
1
+ import os
2
  import speech_recognition as sr
3
+ import logging
4
+ from flask import Flask, render_template_string, request, jsonify
5
  from tempfile import NamedTemporaryFile
 
6
  import ffmpeg
7
+ from fuzzywuzzy import process, fuzz
 
8
 
9
+ # Initialize Flask app
10
  app = Flask(__name__)
11
  logging.basicConfig(level=logging.INFO)
12
 
13
  # Global variables
14
+ cart = [] # Stores items as [item_name, price, quantity] in the cart
15
+ menu_preferences = None # Tracks the current menu preference
16
+ section_preferences = None # Tracks the current section preference
17
+ default_sections = {
18
+ "biryanis": ["veg biryani", "paneer biryani", "chicken biryani", "mutton biryani"],
19
+ "starters": ["samosa", "onion pakoda", "chilli gobi", "chicken manchurian", "veg manchurian"],
20
+ "curries": ["paneer butter", "chicken curry", "fish curry", "chilli chicken"],
21
+ "desserts": ["gulab jamun", "ice cream"],
22
+ "soft drinks": ["cola", "lemon soda"]
23
+ }
24
+ prices = {
25
+ "samosa": 9,
26
+ "onion pakoda": 10,
27
+ "chilli gobi": 12,
28
+ "chicken biryani": 14,
29
+ "mutton biryani": 16,
30
+ "veg biryani": 12,
31
+ "paneer butter": 10,
32
+ "fish curry": 12,
33
+ "chicken manchurian": 14,
34
+ "veg manchurian": 12,
35
+ "chilli chicken": 14,
36
+ "paneer biryani": 13,
37
+ "chicken curry": 14,
38
+ "gulab jamun": 8,
39
+ "ice cream": 6,
40
+ "cola": 5,
41
+ "lemon soda": 6
42
  }
43
+ menus = {
44
+ "all": list(prices.keys()),
45
+ "vegetarian": [
46
+ "samosa", "onion pakoda", "chilli gobi", "veg biryani", "paneer butter", "veg manchurian", "paneer biryani", "gulab jamun", "ice cream", "cola", "lemon soda"
47
+ ],
48
+ "non-vegetarian": [
49
+ "chicken biryani", "mutton biryani", "fish curry", "chicken manchurian", "chilli chicken", "chicken curry", "gulab jamun", "ice cream", "cola", "lemon soda"
50
+ ]
51
+ }
52
+
53
+ @app.route("/")
54
+ def index():
55
+ return render_template_string(html_code)
56
+
57
+ @app.route("/reset-cart", methods=["GET"])
58
+ def reset_cart():
59
+ global cart, menu_preferences, section_preferences
60
+ cart = []
61
+ menu_preferences = None
62
+ section_preferences = None
63
+ return "Cart reset successfully."
64
+
65
+ @app.route("/process-audio", methods=["POST"])
66
+ def process_audio():
67
+ try:
68
+ # Handle audio input
69
+ audio_file = request.files.get("audio")
70
+ if not audio_file:
71
+ return jsonify({"response": "Oops! I didn't catch any audio. Please try again."}), 400
72
+
73
+ # Save and convert audio to WAV format
74
+ temp_file = NamedTemporaryFile(delete=False, suffix=".webm")
75
+ audio_file.save(temp_file.name)
76
+
77
+ converted_file = NamedTemporaryFile(delete=False, suffix=".wav")
78
+ ffmpeg.input(temp_file.name).output(
79
+ converted_file.name, acodec="pcm_s16le", ac=1, ar="16000"
80
+ ).run(overwrite_output=True)
81
+
82
+ # Recognize speech
83
+ recognizer = sr.Recognizer()
84
+ recognizer.dynamic_energy_threshold = True
85
+ recognizer.energy_threshold = 4000 # Increased sensitivity
86
+
87
+ with sr.AudioFile(converted_file.name) as source:
88
+ recognizer.adjust_for_ambient_noise(source, duration=1)
89
+ audio_data = recognizer.record(source)
90
+
91
+ # Try using the Google API for speech recognition
92
+ try:
93
+ raw_command = recognizer.recognize_google(audio_data).lower()
94
+ except sr.UnknownValueError:
95
+ raw_command = "Sorry, I couldn't understand that."
96
+ except sr.RequestError as e:
97
+ raw_command = f"Request error from the service: {e}"
98
+
99
+ logging.info(f"User said: {raw_command}") # Print user speech in the console
100
+
101
+ # Display the transcribed text and AI voice response
102
+ response = process_command(raw_command)
103
+
104
+ except Exception as e:
105
+ response = f"An error occurred: {str(e)}"
106
+ finally:
107
+ os.unlink(temp_file.name)
108
+ os.unlink(converted_file.name)
109
+
110
+ return jsonify({"response": response})
111
+
112
+ def preprocess_command(command):
113
+ """
114
+ Normalize the user command to improve matching.
115
+ """
116
+ command = command.strip().lower()
117
+ return command
118
+
119
+ def process_command(command):
120
+ global cart, menu_preferences, section_preferences
121
+
122
+ # Finalize order
123
+ if "final order" in command or "complete order" in command:
124
+ if not cart:
125
+ return "Your cart is empty. Please add items before finalizing the order."
126
+
127
+ order_summary = "\n".join([f"{item[2]} x {item[0]} for {item[1] * item[2]} INR" for item in cart])
128
+ total_price = sum(item[1] * item[2] for item in cart)
129
+
130
+ cart.clear() # Clear the cart after finalizing
131
+ menu_preferences = None
132
+ section_preferences = None
133
+
134
+ return f"Your order has been placed successfully:\n{order_summary}\nTotal: {total_price} INR.\nThank you for ordering!"
135
+
136
+ # Greet the user and ask for preferences when first started
137
+ if menu_preferences is None:
138
+ if "hello" in command or "hi" in command:
139
+ return "Hello, welcome to Biryani Hub! We have the following categories: Biryanis, Starters, Curries, Desserts, Soft Drinks. Please choose a category."
140
+
141
+ preferences = ["non-vegetarian", "vegetarian", "all"]
142
+ if command in preferences:
143
+ menu_preferences = command
144
+ return f"You've selected the {command} menu! Which section would you like to browse next? (e.g., biryanis, starters, curries, desserts, soft drinks)"
145
+
146
+ # Use fuzzy matching to help recognize similar inputs
147
+ closest_match = process.extractOne(command, preferences, scorer=fuzz.partial_ratio)
148
+ if closest_match and closest_match[1] > 75:
149
+ menu_preferences = closest_match[0]
150
+ return f"Great choice! You've chosen the {menu_preferences} menu. Which section would you like to browse next?"
151
+
152
+ return "I couldn't recognize your choice. Please say either 'Non-Vegetarian', 'Vegetarian', or 'All'."
153
+
154
+ # Handle section preferences and list items
155
+ if section_preferences is None:
156
+ sections = list(default_sections.keys())
157
+ for section in sections:
158
+ if section in command:
159
+ section_preferences = section
160
+ return f"Here are the items in the {section_preferences} section: {', '.join(default_sections[section_preferences])}. Please choose an item."
161
+
162
+ closest_match = process.extractOne(command, sections, scorer=fuzz.partial_ratio)
163
+ if closest_match and closest_match[1] > 75:
164
+ section_preferences = closest_match[0]
165
+ return f"Here are the items in the {section_preferences} section: {', '.join(default_sections[section_preferences])}. What would you like to add?"
166
+
167
+ return "I didn't catch that. Please say a section like 'biryanis', 'starters', 'curries', 'desserts', or 'soft drinks'."
168
+
169
+ # Filter items based on the menu preference (vegetarian/non-vegetarian)
170
+ available_items = []
171
+ if menu_preferences == "vegetarian":
172
+ available_items = [item for item in default_sections[section_preferences] if item in menus["vegetarian"]]
173
+ elif menu_preferences == "non-vegetarian":
174
+ available_items = [item for item in default_sections[section_preferences] if item in menus["non-vegetarian"]]
175
+ elif menu_preferences == "all":
176
+ available_items = [item for item in default_sections[section_preferences]]
177
+
178
+ for item in available_items:
179
+ if item in command:
180
+ quantity = extract_quantity(command)
181
+ if quantity:
182
+ cart.append([item, prices[item], quantity])
183
+ return f"Added {quantity} x {item} to your cart. Your current cart: {', '.join([f'{i[0]} x{i[2]}' for i in cart])}. Would you like to add more items?"
184
+
185
+ return "I didn't recognize the item you mentioned. Please say the item name clearly, or choose from the available items."
186
+
187
+ def extract_quantity(command):
188
+ """
189
+ Extract quantity from the command (e.g., 'two', '3', '5').
190
+ """
191
+ number_words = {
192
+ "one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
193
+ "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
194
+ "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "10": 10
195
+ }
196
+
197
+ command_words = command.split()
198
+ for word in command_words:
199
+ if word in number_words:
200
+ return number_words[word]
201
+ return None
202
 
 
203
  html_code = """
204
  <!DOCTYPE html>
205
  <html lang="en">
 
209
  <title>AI Dining Assistant</title>
210
  <style>
211
  body {
212
+ display: flex;
213
+ flex-direction: column;
214
+ align-items: center;
215
+ justify-content: center;
216
+ min-height: 100vh;
217
+ margin: 0;
218
  font-family: Arial, sans-serif;
 
219
  background-color: #f4f4f9;
220
  }
221
  h1 {
222
  color: #333;
223
  }
224
  .mic-button {
225
+ font-size: 2rem;
226
+ padding: 1rem 2rem;
 
 
227
  color: white;
228
+ background-color: #007bff;
229
  border: none;
230
+ border-radius: 50px;
231
  cursor: pointer;
232
+ transition: background-color 0.3s;
233
+ }
234
+ .mic-button:hover {
235
+ background-color: #0056b3;
236
  }
237
  .status, .response {
238
+ margin-top: 1rem;
239
+ text-align: center;
240
+ color: #555;
241
+ font-size: 1.2rem;
242
+ }
243
+ .response {
244
+ background-color: #e8e8ff;
245
+ padding: 1rem;
246
+ border-radius: 10px;
247
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
248
+ display: none;
249
  }
250
  </style>
251
  </head>
 
253
  <h1>AI Dining Assistant</h1>
254
  <button class="mic-button" id="mic-button">🎤</button>
255
  <div class="status" id="status">Press the mic button to start...</div>
256
+ <div class="response" id="response">Response will appear here...</div>
257
  <script>
258
  const micButton = document.getElementById('mic-button');
259
  const status = document.getElementById('status');
260
  const response = document.getElementById('response');
261
+ let mediaRecorder;
262
+ let audioChunks = [];
263
+ let isConversationActive = false;
264
  micButton.addEventListener('click', () => {
265
+ if (!isConversationActive) {
266
+ isConversationActive = true;
267
+ startConversation();
268
  }
269
  });
270
+ function startConversation() {
271
+ const utterance = new SpeechSynthesisUtterance('Hello, welcome to Biryani Hub! We have the following categories: Biryanis, Starters, Curries, Desserts, Soft Drinks. Please choose a category.');
 
272
  speechSynthesis.speak(utterance);
273
  utterance.onend = () => {
274
+ status.textContent = 'Listening...';
275
  startListening();
276
  };
277
  }
278
+ function startListening() {
279
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
280
+ mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
281
+ mediaRecorder.start();
282
+ audioChunks = [];
283
+ mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
284
+ mediaRecorder.onstop = async () => {
285
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
286
+ const formData = new FormData();
287
+ formData.append('audio', audioBlob);
288
+ status.textContent = 'Processing...';
289
+ try {
290
+ const result = await fetch('/process-audio', { method: 'POST', body: formData });
291
+ const data = await result.json();
292
+ response.textContent = 'You said: ' + data.response; // Display user text
293
+ response.style.display = 'block';
294
+ const utterance = new SpeechSynthesisUtterance(data.response);
295
+ speechSynthesis.speak(utterance);
296
+ utterance.onend = () => {
297
+ console.log("Speech synthesis completed.");
298
+ if (data.response.includes("final order") || data.response.includes("Thank you for ordering")) {
299
+ status.textContent = 'Order completed. Press the mic button to start again.';
300
+ isConversationActive = false;
301
+ } else {
302
+ status.textContent = 'Listening...';
303
+ setTimeout(() => {
304
+ startListening();
305
+ }, 100);
306
+ }
307
+ };
308
+ utterance.onerror = (e) => {
309
+ console.error("Speech synthesis error:", e.error);
310
+ status.textContent = 'Error with speech output.';
311
+ isConversationActive = false;
312
+ };
313
+ } catch (error) {
314
+ response.textContent = 'Sorry, I could not understand. Please try again.';
315
+ response.style.display = 'block';
316
+ status.textContent = 'Press the mic button to restart the conversation.';
317
+ isConversationActive = false;
318
+ }
319
+ };
320
+ setTimeout(() => mediaRecorder.stop(), 5000);
321
+ }).catch(() => {
322
+ status.textContent = 'Microphone access denied.';
323
+ isConversationActive = false;
324
+ });
325
  }
326
  </script>
327
  </body>
328
  </html>
329
  """
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  if __name__ == "__main__":
332
  app.run(host="0.0.0.0", port=7860)