Fix coverage: add /api/coverage route, remove stray gather code from loadCoverage
This commit is contained in:
326
h5_api_server_fixed.py
Normal file
326
h5_api_server_fixed.py
Normal 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)
|
||||
Reference in New Issue
Block a user