· 4 months ago · May 25, 2025, 03:20 PM
1import os
2import tempfile
3import uuid
4from datetime import datetime
5
6import boto3
7import librosa
8import librosa.display
9import matplotlib.pyplot as plt
10import numpy as np
11np.complex = complex # For compatibility with older librosa/numpy if needed
12import scipy.signal
13import tensorflow as tf
14from botocore.exceptions import NoCredentialsError, ClientError
15from flask import Flask, jsonify, request
16from flask_sqlalchemy import SQLAlchemy
17from sqlalchemy import func
18from werkzeug.utils import secure_filename
19
20# --- Configuration ---
21# AWS S3 Configuration
22AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
23AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
24AWS_S3_BUCKET_NAME = os.environ.get('AWS_S3_BUCKET_NAME', 'your-buddybird-bucket') # Thayด้วยชื่อ Bucket ของคุณ
25AWS_S3_REGION = os.environ.get('AWS_S3_REGION', 'ap-southeast-1') # Thayด้วย Region ของ Bucket
26
27# Database Configuration (SQLite for demo, change for MySQL)
28# For MySQL: 'mysql+mysqlconnector://user:password@host/db_name'
29DATABASE_URL = os.environ.get('DATABASE_URL', 'sqlite:///:memory:')
30
31# Model Configuration
32MODEL_PATH = os.environ.get('MODEL_PATH', 'model/BuddyBirdModel.keras')
33LABELS = ['crow', 'koel', 'myna', 'sparrow'] # ควรมาจาก config หรือ database ในอนาคต
34
35# Flask App Initialization
36app = Flask(__name__)
37app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URL
38app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
39app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB limit for uploads
40
41db = SQLAlchemy(app)
42s3_client = boto3.client(
43 's3',
44 aws_access_key_id=AWS_ACCESS_KEY_ID,
45 aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
46 region_name=AWS_S3_REGION
47)
48
49# --- S3 Helper Functions ---
50def upload_file_to_s3(file_path, bucket_name, s3_key, content_type=None):
51 """Uploads a file to an S3 bucket."""
52 try:
53 extra_args = {'ACL': 'public-read'} # ทำให้ไฟล์เข้าถึงได้แบบ public
54 if content_type:
55 extra_args['ContentType'] = content_type
56 s3_client.upload_file(file_path, bucket_name, s3_key, ExtraArgs=extra_args)
57 # Construct public URL (format may vary based on region and bucket settings)
58 url = f"https://{bucket_name}.s3.{AWS_S3_REGION}.amazonaws.com/{s3_key}"
59 return url, s3_key
60 except FileNotFoundError:
61 app.logger.error(f"S3 Upload Error: File not found at {file_path}")
62 return None, None
63 except NoCredentialsError:
64 app.logger.error("S3 Upload Error: Credentials not available.")
65 return None, None
66 except ClientError as e:
67 app.logger.error(f"S3 Client Error: {e}")
68 return None, None
69
70# --- Database Models ---
71class Bird(db.Model):
72 __tablename__ = 'birds'
73 id = db.Column(db.Integer, primary_key=True, autoincrement=True)
74 name = db.Column(db.String(100), nullable=False, unique=True)
75 scientific_name = db.Column(db.String(100), unique=True, nullable=True)
76 description = db.Column(db.Text, nullable=True)
77 habitats = db.Column(db.Text, nullable=True)
78 physical_length_cm = db.Column(db.String(50), nullable=True)
79 physical_wingspan_cm = db.Column(db.String(50), nullable=True)
80 physical_weight_g = db.Column(db.String(50), nullable=True)
81 diet = db.Column(db.Text, nullable=True)
82 image_url = db.Column(db.String(255), nullable=True)
83 conservation_status = db.Column(db.String(50), nullable=True)
84 created_at = db.Column(db.TIMESTAMP, server_default=func.now())
85 updated_at = db.Column(db.TIMESTAMP, server_default=func.now(), onupdate=func.now())
86
87 predictions = db.relationship('Prediction', backref='predicted_bird_obj', lazy=True)
88 suggested_feedbacks = db.relationship('UserFeedback', backref='suggested_bird_obj', lazy=True, foreign_keys='UserFeedback.suggested_bird_id')
89
90class AudioRecording(db.Model):
91 __tablename__ = 'audio_recordings'
92 id = db.Column(db.Integer, primary_key=True, autoincrement=True)
93 original_filename = db.Column(db.String(255), nullable=False)
94 storage_audio_key = db.Column(db.String(255), nullable=False, unique=True)
95 storage_spectrogram_key = db.Column(db.String(255), unique=True, nullable=True)
96 s3_audio_url = db.Column(db.String(512), nullable=True)
97 s3_spectrogram_url = db.Column(db.String(512), nullable=True)
98 duration_seconds = db.Column(db.Float, nullable=True)
99 latitude = db.Column(db.Numeric(10, 8), nullable=True)
100 longitude = db.Column(db.Numeric(11, 8), nullable=True)
101 recorded_at = db.Column(db.DateTime, nullable=True)
102 uploaded_at = db.Column(db.TIMESTAMP, server_default=func.now())
103 processing_status = db.Column(db.Enum('pending', 'processing', 'completed', 'failed', name='processing_status_enum'), default='pending')
104 user_notes = db.Column(db.Text, nullable=True)
105 uploader_ip = db.Column(db.String(45), nullable=True)
106
107 predictions = db.relationship('Prediction', backref='audio_recording_obj', lazy=True, cascade="all, delete-orphan")
108
109class Prediction(db.Model):
110 __tablename__ = 'predictions'
111 id = db.Column(db.Integer, primary_key=True, autoincrement=True)
112 audio_recording_id = db.Column(db.Integer, db.ForeignKey('audio_recordings.id', ondelete='CASCADE'), nullable=False)
113 predicted_bird_id = db.Column(db.Integer, db.ForeignKey('birds.id', ondelete='RESTRICT'), nullable=False)
114 confidence_score = db.Column(db.Float, nullable=False)
115 model_version = db.Column(db.String(50), nullable=False)
116 predicted_at = db.Column(db.TIMESTAMP, server_default=func.now())
117
118 user_feedbacks = db.relationship('UserFeedback', backref='prediction_obj', lazy=True, cascade="all, delete-orphan")
119
120
121class UserFeedback(db.Model):
122 __tablename__ = 'user_feedback'
123 id = db.Column(db.Integer, primary_key=True, autoincrement=True)
124 prediction_id = db.Column(db.Integer, db.ForeignKey('predictions.id', ondelete='CASCADE'), nullable=False, unique=True) # Assuming one feedback per prediction
125 # user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # If you have a users table
126 is_correct_prediction = db.Column(db.Boolean, nullable=True)
127 suggested_bird_id = db.Column(db.Integer, db.ForeignKey('birds.id', ondelete='SET NULL'), nullable=True)
128 feedback_notes = db.Column(db.Text, nullable=True)
129 submitted_at = db.Column(db.TIMESTAMP, server_default=func.now())
130
131
132# --- Load Model ---
133try:
134 model = tf.keras.models.load_model(MODEL_PATH)
135 app.logger.info(f"Model loaded successfully from {MODEL_PATH}")
136except Exception as e:
137 app.logger.error(f"Error loading model: {e}")
138 model = None # Handle cases where model loading fails
139
140# --- Audio Processing Functions (from original code, adapted) ---
141def analyze_bird_audio(y, sr): # Remains the same
142 fft = np.fft.fft(y)
143 magnitude = np.abs(fft)
144 frequency = np.linspace(0, sr, len(magnitude))
145 threshold = np.mean(magnitude[(frequency < 1000) | (frequency > 8000)]) * 3
146 noise_mask = (magnitude > threshold) & ((frequency < 1000) | (frequency > 8000))
147 noise_freq = frequency[noise_mask]
148 low_noise = noise_freq[noise_freq < 500]
149 high_noise = noise_freq[noise_freq > 10000]
150 return {
151 'low_noise': (np.min(low_noise) if len(low_noise) > 0 else None,
152 np.max(low_noise) if len(low_noise) > 0 else None),
153 'high_noise': (np.min(high_noise) if len(high_noise) > 0 else None,
154 np.max(high_noise) if len(high_noise) > 0 else None)
155 }
156
157def adaptive_filter(y, sr, analysis): # Remains the same
158 nyquist = sr / 2
159 if analysis['low_noise'][0] is not None:
160 cutoff = min(300, analysis['low_noise'][1])
161 if 0 < cutoff < nyquist:
162 b, a = scipy.signal.butter(4, cutoff / nyquist, btype='highpass')
163 y = scipy.signal.filtfilt(b, a, y)
164 if analysis['high_noise'][0] is not None:
165 cutoff = max(8000, analysis['high_noise'][0])
166 if 0 < cutoff < nyquist:
167 b, a = scipy.signal.butter(4, cutoff / nyquist, btype='lowpass')
168 y = scipy.signal.filtfilt(b, a, y)
169 return scipy.signal.wiener(y)
170
171def generate_spectrogram_and_features(audio_file_path):
172 """
173 Loads audio, applies filters, generates Mel spectrogram, saves it to S3,
174 and prepares features for the ML model.
175 Returns: (model_input_S_db, s3_spectrogram_url, s3_spectrogram_key, audio_duration)
176 """
177 try:
178 y, sr = librosa.load(audio_file_path, sr=22050)
179 audio_duration = librosa.get_duration(y=y, sr=sr)
180
181 analysis = analyze_bird_audio(y, sr)
182 y_filtered = adaptive_filter(y, sr, analysis)
183 y_filtered = librosa.util.normalize(y_filtered)
184
185 S = librosa.feature.melspectrogram(y=y_filtered, sr=sr, n_fft=2048, hop_length=128, n_mels=128)
186 S_db_full = librosa.power_to_db(S, ref=np.max)
187
188 # --- Save Spectrogram Image to S3 ---
189 s3_spectrogram_url, s3_spectrogram_key = None, None
190 with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_spec_img:
191 plt.figure(figsize=(12, 4))
192 librosa.display.specshow(np.clip(S_db_full, -60, 0), sr=sr, hop_length=128, x_axis='time', y_axis='mel', cmap='magma')
193 plt.colorbar(format='%+2.0f dB')
194 plt.tight_layout()
195 plt.savefig(tmp_spec_img.name, dpi=300, bbox_inches='tight', pad_inches=0)
196 plt.close()
197
198 # Upload spectrogram image to S3
199 s3_spec_filename = f"spectrograms/{uuid.uuid4()}.png"
200 s3_spectrogram_url, s3_spectrogram_key = upload_file_to_s3(
201 tmp_spec_img.name, AWS_S3_BUCKET_NAME, s3_spec_filename, content_type='image/png'
202 )
203 os.remove(tmp_spec_img.name) # Clean up temp image file
204
205 if not s3_spectrogram_url:
206 app.logger.error("Failed to upload spectrogram image to S3.")
207 # Decide if this is a fatal error for the prediction
208
209 # --- Prepare input for AI model ---
210 avg_energy = S_db_full.mean(axis=0)
211 center = np.argmax(avg_energy)
212 start = max(0, center - 64) # 128/2
213 S_db_cropped = S_db_full[:, start:start+128]
214
215 if S_db_cropped.shape[1] < 128:
216 S_db_cropped = np.pad(S_db_cropped, ((0, 0), (0, 128 - S_db_cropped.shape[1])), mode='constant', constant_values=S_db_cropped.min()) # Pad with min value
217
218 model_input_S_db = S_db_cropped.reshape(1, 128, 128, 1)
219
220 return model_input_S_db, s3_spectrogram_url, s3_spectrogram_key, audio_duration
221
222 except Exception as e:
223 app.logger.error(f"Error in generate_spectrogram_and_features: {e}")
224 return None, None, None, None
225
226# --- Helper to get or create Bird ---
227def get_or_create_bird(name, scientific_name=None):
228 bird = Bird.query.filter_by(name=name).first()
229 if not bird:
230 bird = Bird(name=name, scientific_name=scientific_name) # Add other details if known
231 db.session.add(bird)
232 # db.session.commit() # Commit separately or as part of a larger transaction
233 return bird
234
235# --- API Endpoints ---
236@app.route('/api/v1/predictions', methods=['POST'])
237def create_prediction():
238 if model is None:
239 return jsonify({"error": "Model not loaded, prediction unavailable."}), 503
240
241 if 'file' not in request.files:
242 return jsonify({"error": "No audio file part"}), 400
243
244 audio_file = request.files['file']
245 if audio_file.filename == '':
246 return jsonify({"error": "No selected audio file"}), 400
247
248 if not AWS_S3_BUCKET_NAME:
249 return jsonify({"error": "S3 Bucket not configured."}), 500
250
251 filename = secure_filename(audio_file.filename)
252 s3_audio_url, s3_spectrogram_url = None, None
253 s3_audio_key, s3_spectrogram_key = None, None
254 audio_recording_id = None
255 temp_audio_path = None
256
257 try:
258 # 1. Save uploaded audio to a temporary file
259 with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as tmp_audio:
260 audio_file.save(tmp_audio.name)
261 temp_audio_path = tmp_audio.name
262
263 # 2. Upload original audio to S3
264 s3_audio_filename = f"audio_uploads/{uuid.uuid4()}{os.path.splitext(filename)[1]}"
265 s3_audio_url, s3_audio_key = upload_file_to_s3(
266 temp_audio_path, AWS_S3_BUCKET_NAME, s3_audio_filename, content_type=audio_file.content_type
267 )
268 if not s3_audio_url:
269 raise Exception("Failed to upload audio to S3")
270
271 # 3. Create initial AudioRecording entry
272 new_audio_recording = AudioRecording(
273 original_filename=filename,
274 storage_audio_key=s3_audio_key,
275 s3_audio_url=s3_audio_url,
276 latitude=request.form.get('latitude', type=float),
277 longitude=request.form.get('longitude', type=float),
278 recorded_at=datetime.fromisoformat(request.form.get('recorded_at')) if request.form.get('recorded_at') else None,
279 user_notes=request.form.get('user_notes'),
280 uploader_ip=request.remote_addr,
281 processing_status='processing'
282 )
283 db.session.add(new_audio_recording)
284 db.session.commit() # Commit to get ID
285 audio_recording_id = new_audio_recording.id
286
287 # 4. Generate spectrogram and features
288 model_input, s3_spectrogram_url, s3_spectrogram_key, audio_duration = generate_spectrogram_and_features(temp_audio_path)
289
290 if model_input is None:
291 new_audio_recording.processing_status = 'failed'
292 db.session.commit()
293 raise Exception("Failed to generate spectrogram or features")
294
295 # Update AudioRecording with spectrogram info and duration
296 new_audio_recording.storage_spectrogram_key = s3_spectrogram_key
297 new_audio_recording.s3_spectrogram_url = s3_spectrogram_url
298 new_audio_recording.duration_seconds = audio_duration
299
300 # 5. Make prediction
301 pred_probs = model.predict(model_input)
302 predicted_label_index = np.argmax(pred_probs[0])
303 predicted_label_name = LABELS[predicted_label_index]
304 confidence = float(np.max(pred_probs[0]))
305
306 # 6. Get or create Bird in DB
307 predicted_bird = get_or_create_bird(predicted_label_name)
308 db.session.flush() # Ensure bird object has ID if newly created, before commit
309
310 # 7. Save Prediction to DB
311 new_prediction = Prediction(
312 audio_recording_id=new_audio_recording.id,
313 predicted_bird_id=predicted_bird.id,
314 confidence_score=confidence,
315 model_version="BuddyBirdModel_vCurrent" # Get from config or model metadata
316 )
317 db.session.add(new_prediction)
318 new_audio_recording.processing_status = 'completed'
319 db.session.commit()
320
321 return jsonify({
322 "message": "Prediction successful",
323 "prediction_id": new_prediction.id,
324 "audio_recording_id": new_audio_recording.id,
325 "predicted_bird": {
326 "id": predicted_bird.id,
327 "name": predicted_bird.name,
328 "scientific_name": predicted_bird.scientific_name
329 },
330 "confidence_score": confidence,
331 "model_version": new_prediction.model_version,
332 "audio_url": new_audio_recording.s3_audio_url,
333 "spectrogram_url": new_audio_recording.s3_spectrogram_url
334 }), 201
335
336 except Exception as e:
337 app.logger.error(f"Error in create_prediction: {e}")
338 if audio_recording_id: # If audio record was created, mark as failed
339 try:
340 audio_rec = AudioRecording.query.get(audio_recording_id)
341 if audio_rec and audio_rec.processing_status != 'completed':
342 audio_rec.processing_status = 'failed'
343 db.session.commit()
344 except Exception as db_err:
345 app.logger.error(f"Failed to update audio record status on error: {db_err}")
346 db.session.rollback()
347 return jsonify({"error": "An internal error occurred", "details": str(e)}), 500
348 finally:
349 if temp_audio_path and os.path.exists(temp_audio_path):
350 os.remove(temp_audio_path) # Clean up temp audio file
351
352@app.route('/api/v1/predictions', methods=['GET'])
353def list_predictions():
354 page = request.args.get('page', 1, type=int)
355 per_page = request.args.get('per_page', 10, type=int)
356
357 query = Prediction.query.join(AudioRecording).join(Bird, Prediction.predicted_bird_id == Bird.id)
358
359 # Optional Filters
360 if 'bird_id' in request.args:
361 query = query.filter(Prediction.predicted_bird_id == request.args.get('bird_id', type=int))
362 if 'min_confidence' in request.args:
363 query = query.filter(Prediction.confidence_score >= request.args.get('min_confidence', type=float))
364 if 'start_date' in request.args:
365 try:
366 start_date = datetime.fromisoformat(request.args.get('start_date'))
367 query = query.filter(Prediction.predicted_at >= start_date)
368 except ValueError:
369 return jsonify({"error": "Invalid start_date format. Use YYYY-MM-DD or ISO 8601."}), 400
370 if 'end_date' in request.args:
371 try:
372 end_date = datetime.fromisoformat(request.args.get('end_date'))
373 query = query.filter(Prediction.predicted_at <= end_date)
374 except ValueError:
375 return jsonify({"error": "Invalid end_date format. Use YYYY-MM-DD or ISO 8601."}), 400
376
377 predictions_page = query.order_by(Prediction.predicted_at.desc()).paginate(page=page, per_page=per_page, error_out=False)
378
379 results = []
380 for p in predictions_page.items:
381 results.append({
382 "prediction_id": p.id,
383 "audio_recording_id": p.audio_recording_id,
384 "predicted_at": p.predicted_at.isoformat() + 'Z',
385 "predicted_bird": {
386 "id": p.predicted_bird_obj.id,
387 "name": p.predicted_bird_obj.name
388 },
389 "confidence_score": p.confidence_score,
390 "model_version": p.model_version,
391 "audio_url": p.audio_recording_obj.s3_audio_url,
392 "spectrogram_url": p.audio_recording_obj.s3_spectrogram_url
393 })
394
395 return jsonify({
396 "data": results,
397 "pagination": {
398 "total_items": predictions_page.total,
399 "total_pages": predictions_page.pages,
400 "current_page": predictions_page.page,
401 "per_page": predictions_page.per_page
402 }
403 })
404
405@app.route('/api/v1/predictions/<int:prediction_id>', methods=['GET'])
406def get_prediction_by_id(prediction_id):
407 pred = Prediction.query.get(prediction_id)
408 if not pred:
409 return jsonify({"error": "Prediction not found"}), 404
410
411 audio_rec = pred.audio_recording_obj
412 bird_details = pred.predicted_bird_obj
413 feedback = UserFeedback.query.filter_by(prediction_id=pred.id).first()
414
415 feedback_data = None
416 if feedback:
417 suggested_bird_data = None
418 if feedback.suggested_bird_id and feedback.suggested_bird_obj:
419 suggested_bird_data = {
420 "id": feedback.suggested_bird_obj.id,
421 "name": feedback.suggested_bird_obj.name,
422 "scientific_name": feedback.suggested_bird_obj.scientific_name
423 }
424 feedback_data = {
425 "feedback_id": feedback.id,
426 "is_correct_prediction": feedback.is_correct_prediction,
427 "suggested_bird_details": suggested_bird_data,
428 "feedback_notes": feedback.feedback_notes,
429 "submitted_at": feedback.submitted_at.isoformat() + 'Z'
430 }
431
432 return jsonify({
433 "prediction_id": pred.id,
434 "model_version": pred.model_version,
435 "predicted_at": pred.predicted_at.isoformat() + 'Z',
436 "confidence_score": pred.confidence_score,
437 "predicted_bird_details": {
438 "id": bird_details.id,
439 "name": bird_details.name,
440 "scientific_name": bird_details.scientific_name,
441 "description": bird_details.description,
442 "habitats": bird_details.habitats,
443 "physical_length_cm": bird_details.physical_length_cm,
444 "image_url": bird_details.image_url
445 },
446 "audio_recording_details": {
447 "id": audio_rec.id,
448 "original_filename": audio_rec.original_filename,
449 "storage_audio_key": audio_rec.storage_audio_key,
450 "s3_audio_url": audio_rec.s3_audio_url,
451 "storage_spectrogram_key": audio_rec.storage_spectrogram_key,
452 "s3_spectrogram_url": audio_rec.s3_spectrogram_url,
453 "duration_seconds": audio_rec.duration_seconds,
454 "latitude": float(audio_rec.latitude) if audio_rec.latitude else None,
455 "longitude": float(audio_rec.longitude) if audio_rec.longitude else None,
456 "recorded_at": audio_rec.recorded_at.isoformat() + 'Z' if audio_rec.recorded_at else None,
457 "uploaded_at": audio_rec.uploaded_at.isoformat() + 'Z',
458 "user_notes": audio_rec.user_notes
459 },
460 "user_feedback": feedback_data
461 })
462
463@app.route('/api/v1/feedback', methods=['POST'])
464def submit_feedback():
465 data = request.get_json()
466 if not data or 'prediction_id' not in data:
467 return jsonify({"error": "Missing prediction_id"}), 400
468
469 prediction_id = data.get('prediction_id')
470 pred = Prediction.query.get(prediction_id)
471 if not pred:
472 return jsonify({"error": f"Prediction with id {prediction_id} not found"}), 404
473
474 # Check if feedback for this prediction already exists
475 existing_feedback = UserFeedback.query.filter_by(prediction_id=prediction_id).first()
476 if existing_feedback:
477 return jsonify({"error": f"Feedback for prediction {prediction_id} already exists. Consider PUT to update."}), 409 # Conflict
478
479 is_correct = data.get('is_correct_prediction') # Can be True, False, or None
480 suggested_bird_id = data.get('suggested_bird_id')
481 feedback_notes = data.get('feedback_notes')
482
483 if suggested_bird_id:
484 suggested_bird = Bird.query.get(suggested_bird_id)
485 if not suggested_bird:
486 return jsonify({"error": f"Suggested bird with id {suggested_bird_id} not found"}), 404
487
488 new_feedback = UserFeedback(
489 prediction_id=prediction_id,
490 is_correct_prediction=is_correct,
491 suggested_bird_id=suggested_bird_id,
492 feedback_notes=feedback_notes
493 # user_id=data.get('user_id') # If you have user auth
494 )
495 db.session.add(new_feedback)
496 db.session.commit()
497
498 return jsonify({
499 "message": "Feedback submitted successfully",
500 "feedback_id": new_feedback.id
501 }), 201
502
503@app.route('/api/v1/birds', methods=['GET'])
504def list_birds():
505 birds = Bird.query.all()
506 results = [{
507 "id": bird.id,
508 "name": bird.name,
509 "scientific_name": bird.scientific_name,
510 "image_url": bird.image_url
511 } for bird in birds]
512 return jsonify({"data": results})
513
514@app.route('/api/v1/birds/<int:bird_id>', methods=['GET'])
515def get_bird_by_id(bird_id):
516 bird = Bird.query.get(bird_id)
517 if not bird:
518 return jsonify({"error": "Bird not found"}), 404
519
520 return jsonify({
521 "id": bird.id,
522 "name": bird.name,
523 "scientific_name": bird.scientific_name,
524 "description": bird.description,
525 "habitats": bird.habitats,
526 "physical_length_cm": bird.physical_length_cm,
527 "physical_wingspan_cm": bird.physical_wingspan_cm,
528 "physical_weight_g": bird.physical_weight_g,
529 "diet": bird.diet,
530 "image_url": bird.image_url,
531 "conservation_status": bird.conservation_status
532 })
533
534# --- Initialize DB and Add Initial Birds (for demo) ---
535def init_db():
536 with app.app_context():
537 db.create_all()
538 # Add initial birds if they don't exist
539 for label_name in LABELS:
540 bird = Bird.query.filter_by(name=label_name).first()
541 if not bird:
542 # You can add more details for each bird here if known
543 bird = Bird(name=label_name, scientific_name=f"Scientificus {label_name.capitalize()}")
544 db.session.add(bird)
545 db.session.commit()
546 app.logger.info("Database initialized and initial birds checked/added.")
547
548if __name__ == '__main__':
549 init_db() # Initialize database and add initial birds
550 app.run(debug=True, host='0.0.0.0', port=5000)