#!/usr/bin/env python3 from flask import Flask, jsonify, request from flask_cors import CORS import json import h5py import numpy as np from pathlib import Path from datetime import datetime app = Flask(__name__) CORS(app) # Load index once at startup with open('/data/index.json', 'r') as f: INDEX = json.load(f) def to_python_type(val): """Convert numpy types to Python types for JSON serialization""" if hasattr(val, 'item'): return val.item() return val H5_DIR = Path('/data/h5') @app.route('/api/nodes', methods=['GET']) def get_nodes(): nodes_list = [] for node_id, node_info in INDEX['nodes'].items(): file_count = len(node_info.get('files', [])) nodes_list.append({ 'id': node_id, 'position': node_info.get('position', {}), 'file_count': file_count, 'hasDates': file_count > 0 }) return jsonify({ 'nodes': nodes_list, 'sampleRateHz': 500 }) @app.route('/api/dates', methods=['GET']) def get_dates(): return jsonify({'dates': INDEX['dates']}) @app.route('/api/migration-status', methods=['GET']) def get_migration_status(): """Status de la conversion RAW -> H5""" total_files = 345 # Connu du projet h5_files = list(H5_DIR.glob('*.h5')) converted = len(h5_files) return jsonify({ 'summary': { 'total_files': total_files, 'converted_files': converted, 'percentage': round(converted / total_files * 100, 1), 'status': 'in_progress' if converted < total_files else 'complete' }, 'h5_files_available': converted }) @app.route('/api/rms-timeline', methods=['GET']) def get_rms_timeline(): """Timeline RMS optimisé - utilise l'INDEX pour filtrer par date""" date = request.args.get('date') channel = request.args.get('channel', 'ch0') window_sec = int(request.args.get('window', 60)) if not date: return jsonify({'error': 'date parameter required'}), 400 channel_num = int(channel.replace('ch', '').replace('CH', '')) nodes_data = {} # Utiliser l'INDEX pour trouver les fichiers de cette date for node_id, node_info in INDEX['nodes'].items(): for file_info in node_info.get('files', []): if file_info.get('date') != date: continue filename = file_info.get('filename') h5_path = H5_DIR / filename if not h5_path.exists(): continue try: with h5py.File(h5_path, 'r') as f: sample_rate = to_python_type(f['metadata'].attrs.get('sample_rate_hz', 500)) start_ts = file_info.get('timestamp', 0) dataset_name = f'calibrated_data/channel_{channel_num + 1}' if dataset_name not in f: continue dataset = f[dataset_name] total_samples = dataset.shape[0] window_samples = int(window_sec * sample_rate) # Calculer RMS par fenêtre (max 100 points par fichier) timeline = [] step = max(window_samples, total_samples // 100) for i in range(0, min(total_samples, step * 100), step): end_idx = min(i + window_samples, total_samples) chunk = dataset[i:end_idx] rms = float(np.sqrt(np.mean(chunk ** 2))) ts = start_ts + (i / sample_rate) timeline.append({'ts': ts, 'rms': rms}) if node_id not in nodes_data: nodes_data[node_id] = [] nodes_data[node_id].extend(timeline) except Exception as e: continue return jsonify({ 'date': date, 'channel': channel, 'nodes': nodes_data }) @app.route('/api/h5/data', methods=['GET']) @app.route('/api/data', methods=['GET']) def get_waveform_data(): """Données waveform - accepte soit file= soit node=+date=""" filename = request.args.get('file') node_id = request.args.get('node') date = request.args.get('date') channel = request.args.get('channel', 'ch0') start = float(request.args.get('start', 0)) duration = float(request.args.get('duration', 10)) # Trouver le fichier H5 h5_file = None if filename: # Mode direct: fichier spécifié h5_file = H5_DIR / filename elif node_id and date: # Mode lookup: chercher via node_id et date node_info = INDEX['nodes'].get(node_id) if node_info: for file_info in node_info.get('files', []): if file_info.get('date') == date: h5_file = H5_DIR / file_info.get('filename', '') break else: return jsonify({'error': 'file or (node and date) required'}), 400 if not h5_file or not h5_file.exists(): return jsonify({'error': 'H5 file not found', 'path': str(h5_file)}), 404 try: with h5py.File(h5_file, 'r') as f: sample_rate = f['metadata'].attrs['sample_rate_hz'] channel_num = int(channel.replace('ch', '').replace('CH', '')) # Lire les données calibrées dataset = f[f'calibrated_data/channel_{channel_num + 1}'] start_sample = int(start * sample_rate) end_sample = int((start + duration) * sample_rate) # Limiter à la taille du dataset end_sample = min(end_sample, dataset.shape[0]) if start_sample >= dataset.shape[0]: return jsonify({'error': 'start time out of range'}), 400 data = dataset[start_sample:end_sample] # Calculer stats rms = float(np.sqrt(np.mean(data ** 2))) peak = float(np.max(np.abs(data))) return jsonify({ 'node_id': node_id, 'date': date, 'channel': channel, 'start': start, 'duration': duration, 'sample_rate': to_python_type(sample_rate), 'data': data.tolist(), 'stats': { 'rms': rms, 'peak': peak, 'samples': len(data) } }) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/h5/files', methods=['GET']) def get_h5_files(): """Liste des fichiers H5 disponibles avec métadonnées""" files = [] for h5_path in H5_DIR.glob('*.h5'): try: with h5py.File(h5_path, 'r') as f: filename = h5_path.name # Parse: auto_228_064210_b50_rsn51848_seq1_1597256250.h5 parts = filename.replace('.h5', '').split('_') node_id = parts[1] if len(parts) > 1 else 'unknown' # Find date from INDEX date = 'unknown' node_info = INDEX['nodes'].get(node_id) if node_info: for file_info in node_info.get('files', []): if file_info.get('filename') == filename: date = file_info.get('date', 'unknown') break files.append({ 'filename': filename, 'nodeId': node_id, 'date': date, 'size_mb': round(h5_path.stat().st_size / 1024 / 1024, 2), 'duration_sec': to_python_type(f['metadata'].attrs.get('duration_sec', 0)), 'sample_rate': to_python_type(f['metadata'].attrs.get('sample_rate_hz', 500)), 'channels': to_python_type(f['metadata'].attrs.get('n_channels', 4)) }) except Exception as e: pass return jsonify({'files': files, 'count': len(files)}) @app.route('/api/h5/coverage', methods=['GET']) def get_h5_coverage(): """Matrice de couverture nodes x dates""" coverage = {} for node_id, node_info in INDEX['nodes'].items(): node_dates = [f['date'] for f in node_info.get('files', []) if 'date' in f] coverage[node_id] = node_dates return jsonify({ 'coverage': coverage, 'total_nodes': len(coverage), 'total_dates': len(INDEX['dates']) }) @app.route('/api/h5/gaps', methods=['GET']) def get_h5_gaps(): """Identify gaps in H5 data coverage per node""" gaps = [] for node_id, node_info in INDEX['nodes'].items(): files = sorted(node_info.get('files', []), key=lambda x: x.get('timestamp', 0)) for i in range(len(files) - 1): current = files[i] next_file = files[i + 1] current_end = current.get('timestamp', 0) + current.get('duration_sec', 0) next_start = next_file.get('timestamp', 0) gap_seconds = next_start - current_end # Report gaps > 1 hour if gap_seconds > 3600: gaps.append({ 'node_id': node_id, 'gap_start': current_end, 'gap_end': next_start, 'gap_hours': round(gap_seconds / 3600, 2), 'before_file': current.get('filename'), 'after_file': next_file.get('filename') }) return jsonify({ 'gaps': gaps, 'total_gaps': len(gaps) }) @app.route('/api/chat', methods=['POST']) def chat(): """Endpoint chat assistant (mock)""" data = request.json message = data.get('message', '') return jsonify({ 'response': f"[Mock] Vous avez dit: {message}", 'timestamp': datetime.now().isoformat() }) @app.route('/api/docs/manifest', methods=['GET']) def get_docs_manifest(): """Manifest de la documentation de campagne""" return jsonify({ 'campaign': { 'name': 'SeaKESP Sète 2020', 'start_date': '2020-07-11', 'end_date': '2020-09-22', 'duration_days': 46, 'location': 'Sète, Méditerranée, France', 'coordinates': {'lat': 43.40, 'lon': 3.70} }, 'timeline': [ {'date': '2020-07-11', 'event': 'Début du déploiement', 'type': 'deployment'}, {'date': '2020-08-08', 'event': 'Premiers enregistrements', 'type': 'recording'}, {'date': '2020-08-12', 'event': 'Campagne principale', 'type': 'recording'}, {'date': '2020-08-16', 'event': 'Fin campagne principale', 'type': 'recording'}, {'date': '2020-09-22', 'event': 'Récupération équipements', 'type': 'recovery'} ], 'documents': [ {'name': 'Rapport Gandalf', 'type': 'report', 'available': False}, {'name': 'SPS Preplots', 'type': 'technical', 'available': False}, {'name': 'Paramètres acquisition', 'type': 'technical', 'available': False} ], 'stats': { 'total_nodes': len(INDEX['nodes']), 'total_dates': len(INDEX['dates']), 'total_files': INDEX.get('total_files', 0), 'sample_rate_hz': 500, 'channels': 4 } }) @app.route('/health', methods=['GET']) def health(): return jsonify({'status': 'ok', 'nodes': len(INDEX['nodes']), 'dates': len(INDEX['dates'])}) if __name__ == '__main__': print(f"Loaded {len(INDEX['nodes'])} nodes, {len(INDEX['dates'])} dates") print(f"H5 directory: {H5_DIR}") app.run(host='0.0.0.0', port=3004)