Fix coverage: add /api/coverage route, remove stray gather code from loadCoverage

This commit is contained in:
Floppyrj45
2026-02-19 14:53:10 +01:00
parent 61b25ab734
commit bbd6a22b57
80 changed files with 27884 additions and 1 deletions

326
h5_api_server_fixed.py Normal file
View File

@@ -0,0 +1,326 @@
#!/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)