import birdvoxclassify
import collections
from contextlib import redirect_stderr
import datetime
import h5py
import hashlib
import joblib
import json
import librosa
import logging
import numpy as np
import operator
import os
import pandas as pd
import platform
import scipy
import scipy.signal
import sklearn
import socket
import soundfile as sf
import sys
import time
import traceback
import warnings

# Width of the spectrogram matrix as input to the convnets
BVD_CLIP_LENGTH = 104

with warnings.catch_warnings():
    # Suppress TF and Keras warnings when importing
    os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
    warnings.simplefilter("ignore")
    import tensorflow as tf

    tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
    with redirect_stderr(open(os.devnull, "w")):
        from tensorflow import keras

import birdvoxdetect
from birdvoxdetect.birdvoxdetect_exceptions import BirdVoxDetectError

def map_tfr(x_tfr):
    return np.log1p(0.5 * x_tfr) - 0.8

def process_file(
    filepath,
    output_dir=None,
    export_clips=False,
    export_confidence=False,
    export_context=False,
    export_faults=False,
    export_logger=False,
    predict_proba=False,
    threshold=50.0,
    suffix="",
    clip_duration=1.0,
    logger_level=logging.INFO,
    detector_name="birdvoxdetect-v03_trial-12_network_epoch-068",
    classifier_name="_".join(
        [
            "birdvoxclassify-flat-multitask-convnet-v2",
            "tv1hierarchical-3c6d869456b2705ea5805b6b7d08f870",
        ]
    ),
    custom_objects=None,
    bva_threshold=0.5,
):
    # Record local time. This will eventually serve to measure elapsed time.
    start_time = time.time()

    # Create output_dir if necessary.
    if output_dir is not None:
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

    # Append underscore to suffix if it is not empty.
    if len(suffix) > 0 and not suffix[-1] == "_":
        suffix = suffix + "_"

    # Set logger level.
    logger = logging.getLogger("BirdVoxDetect")
    logger.setLevel(logger_level)
    if export_logger:
        logger_path = get_output_path(
            filepath, suffix + "logger.txt", output_dir=output_dir
        )
        logging.basicConfig(filename=logger_path, filemode="w", level=logger_level)

    # Print new line and file name.
    logger.info("-" * 80)
    modules = [
        birdvoxdetect,
        birdvoxclassify,
        h5py,
        joblib,
        json,
        librosa,
        logging,
        np,
        pd,
        tf,
        scipy,
        sf,
        sklearn,
    ]
    for module in modules:
        logger.info(module.__name__.ljust(15) + " v" + module.__version__)
    logger.info("")
    logger.info("Loading file: {}".format(filepath))

    # Check for existence of the input file.
    if not os.path.exists(filepath):
        raise BirdVoxDetectError('File "{}" could not be found.'.format(filepath))

    # Load the file.
    try:
        sound_file = sf.SoundFile(filepath)
    except Exception:
        raise BirdVoxDetectError("Could not open file: {}".format(filepath))

    # Load the detector of sensor faults.
    sensorfault_detector_name = "birdvoxactivate.pkl"
    logger.info("Sensor fault detector: {}".format(sensorfault_detector_name))
    sensorfault_model_path = get_model_path(sensorfault_detector_name)
    if not os.path.exists(sensorfault_model_path):
        raise BirdVoxDetectError(
            'Model "{}" could not be found.'.format(sensorfault_detector_name)
        )
    sensorfault_model = joblib.load(sensorfault_model_path)

    # Load the detector of flight calls.
    logger.info("Flight call detector: {}".format(detector_name))
    if detector_name == "pcen_snr":
        detector = "pcen_snr"
    else:
        detector_model_path = get_model_path(detector_name + ".h5")
        if not os.path.exists(detector_model_path):
            raise BirdVoxDetectError(
                'Model "{}" could not be found.'.format(detector_name)
            )
        MAX_LOAD_ATTEMPTS = 10
        load_attempt_id = 0
        is_load_successful = False
        while not is_load_successful and (load_attempt_id < MAX_LOAD_ATTEMPTS):
            try:
                with warnings.catch_warnings():
                    warnings.simplefilter("ignore")
                    detector = keras.models.load_model(
                        detector_model_path, custom_objects=custom_objects
                    )
                is_load_successful = True
            except Exception:
                load_attempt_id += 1
        if not is_load_successful:
            exc_str = 'Could not open detector model "{}":\n{}'
            formatted_trace = traceback.format_exc()
            exc_formatted_str = exc_str.format(detector_model_path, formatted_trace)
            raise BirdVoxDetectError(exc_formatted_str)

    # Load the species classifier.
    logger.info("Species classifier: {}".format(classifier_name))
    classifier_model_path = birdvoxclassify.get_model_path(classifier_name)
    if not os.path.exists(classifier_model_path):
        raise BirdVoxDetectError(
            'Model "{}" could not be found.'.format(classifier_name)
        )
    load_attempt_id = 0
    is_load_successful = False
    while not is_load_successful and (load_attempt_id < MAX_LOAD_ATTEMPTS):
        try:
            with warnings.catch_warnings():
                warnings.simplefilter("ignore")
                classifier = keras.models.load_model(
                    classifier_model_path, compile=False, custom_objects=custom_objects
                )
            is_load_successful = True
        except Exception:
            load_attempt_id += 1
    if not is_load_successful:
        exc_str = 'Could not open classifier model "{}":\n{}'
        formatted_trace = traceback.format_exc()
        exc_formatted_str = exc_str.format(classifier_model_path, formatted_trace)
        raise BirdVoxDetectError(exc_formatted_str)

    # Load the taxonomy.
    taxonomy_path = birdvoxclassify.get_taxonomy_path(classifier_name)
    taxonomy = birdvoxclassify.load_taxonomy(taxonomy_path)

    # Define percentiles.
    percentiles = [0.1, 1, 10, 25, 50, 75, 90, 99, 99.9]

    # Define chunk size.
    has_context = "T-" in detector_name
    if has_context:
        queue_length = 4
        T_str = detector_name.split("T-")[1].split("_")[0]
        chunk_duration = int(T_str) / queue_length
    else:
        chunk_duration = 450
        queue_length = 1

    # Define minimum peak height for BirdVoxDetect function.
    min_peak_height = min(10, threshold)

    # Define number of chunks.
    sr = sound_file.samplerate
    chunk_length = int(chunk_duration * sr)
    full_length = len(sound_file)
    n_chunks = max(1, int(np.ceil(full_length) / chunk_length))

    # Define frame rate.
    pcen_settings = get_pcen_settings()
    frame_rate = pcen_settings["sr"] / (
        pcen_settings["hop_length"] * pcen_settings["stride_length"]
    )

    # Initialize checklist as a Pandas DataFrame.
    if threshold is not None:
        checklist_path = get_output_path(
            filepath, suffix + "checklist.csv", output_dir=output_dir
        )
        event_hhmmss = []
        event_4lettercodes = []
        event_confidences = []
        df_columns = [
            "Time (hh:mm:ss)",
            "Detection confidence (%)",
            "Order",
            "Order confidence (%)",
            "Family",
            "Family confidence (%)",
            "Species (English name)",
            "Species (scientific name)",
            "Species (4-letter code)",
            "Species confidence (%)",
        ]
        df = pd.DataFrame(columns=df_columns)
        df.to_csv(checklist_path, index=False)

    # Initialize fault log as a Pandas DataFrame.
    faultlist_path = get_output_path(
        filepath, suffix + "faults.csv", output_dir=output_dir
    )
    faultlist_df_columns = [
        "Start (hh:mm:ss)",
        "Stop (hh:mm:ss)",
        "Fault confidence (%)",
    ]
    faultlist_df = pd.DataFrame(columns=faultlist_df_columns)
    if export_faults:
        faultlist_df.to_csv(
            faultlist_path,
            columns=faultlist_df_columns, index=False, float_format="%.2f%%"
        )

    # Initialize JSON output.
    if predict_proba:
        json_path = get_output_path(filepath, suffix + "proba.json", output_dir)
        # Get MD5 hash.
        hash_md5 = hashlib.md5()
        with open(filepath, "rb") as fhandle:
            for chunk in iter(lambda: fhandle.read(4096), b""):
                hash_md5.update(chunk)
        json_metadata = {
            "file_name": os.path.basename(filepath),
            "file_path": os.path.abspath(filepath),
            "audio_duration": librosa.get_duration(filename=filepath),
            "audio_md5_checksum": hash_md5.hexdigest(),
            "birdvoxdetect_threshold": threshold,
            "birdvoxactivate_threshold": bva_threshold,
            "classifier_name": classifier_name,
            "detector_name": detector_name,
            "hostname": socket.gethostname(),
            "machine_time": datetime.datetime.now().astimezone().isoformat(),
            "package_versions": {
                module.__name__: module.__version__ for module in modules
            },
            "platform_machine": platform.machine(),
            "platform_processor": platform.processor(),
            "platform_release": platform.release(),
            "platform_system": platform.system(),
            "platform_version": platform.version(),
            "sys_version": sys.version,
        }
        with open(json_path, "w") as f:
            json.dump({"metadata": json_metadata, "taxonomy": taxonomy}, f)
        json_dicts = []

    # Create directory of output clips.
    if export_clips:
        clips_dir = get_output_path(filepath, suffix + "clips", output_dir=output_dir)
        if not os.path.exists(clips_dir):
            os.makedirs(clips_dir)

    # Initialize list of per-chunk confidences.
    if export_confidence:
        chunk_confidences = []

    # Initialize list of context arrays.
    if export_context:
        contexts = []

    # Print chunk duration.
    logger.info("Chunk duration: {} seconds".format(chunk_duration))
    logger.info("")

    # Define padding. Set to one second, i.e. 750 hops @ 24 kHz.
    # Any value above clip duration (150 ms) would work.
    chunk_padding = pcen_settings["hop_length"] * int(
        pcen_settings["sr"] / pcen_settings["hop_length"]
    )

    # Pre-load double-ended queue.
    deque = collections.deque()
    for chunk_id in range(min(n_chunks - 1, queue_length)):
        # Read audio chunk.
        chunk_start = chunk_id * chunk_length
        sound_file.seek(chunk_start)
        chunk_audio = sound_file.read(chunk_length + chunk_padding)

        # Compute PCEN.
        chunk_pcen = compute_pcen(chunk_audio, sr)
        deque.append(chunk_pcen)

    # Compute context.
    if n_chunks > 1:
        concat_deque = np.concatenate(deque, axis=1)
        deque_context = np.percentile(
            concat_deque, percentiles, axis=1, overwrite_input=True
        )

        # Append context.
        if export_context:
            contexts.append(deque_context)

        # Compute sensor fault features.
        # Median is 4th order statistic. Restrict to lowest 120 mel-freq bins
        context_median = deque_context[4, :120]
        context_median_medfilt = scipy.signal.medfilt(context_median, kernel_size=(13,))
        sensorfault_features = context_median_medfilt[::12].reshape(1, -1)

        # Compute probability of sensor fault.
        sensor_fault_probability = sensorfault_model.predict_proba(
            sensorfault_features
        )[0][1]

        # If probability of sensor fault is above 50%,
        # exclude start of recording
        if sensor_fault_probability > bva_threshold:
            logger.info(
                "Probability of sensor fault: {:5.2f}%".format(
                    100 * sensor_fault_probability
                )
            )
            chunk_id_start = min(n_chunks - 1, queue_length)
            context_duration = chunk_duration
            context_duration_str = str(datetime.timedelta(seconds=context_duration))
            logger.info(
                "Ignoring segment between 00:00:00 and "
                + context_duration_str
                + " ("
                + str(chunk_id_start)
                + " chunks)"
            )
            has_sensor_fault = True
            # If continuous confidence is required, store it in memory.
            if export_confidence:
                chunk_confidence = np.full(
                    int(chunk_id_start * chunk_duration * frame_rate), np.nan
                )
                chunk_confidences.append(chunk_confidence)
        else:
            chunk_id_start = 0
            has_sensor_fault = False

        # Add first row to sensor fault log.
        faultlist_df = faultlist_df.append(
            {
                "Start (hh:mm:ss)": seconds_to_hhmmss(0.0),
                "Stop (hh:mm:ss)": seconds_to_hhmmss(queue_length * chunk_duration),
                "Fault confidence (%)": int(sensor_fault_probability * 100),
            },
            ignore_index=True,
        )
        if export_faults:
            faultlist_df.to_csv(
                faultlist_path, columns=faultlist_df_columns,
                index=False, float_format="%.2f%%"
            )
    else:
        chunk_id_start = 0
        has_sensor_fault = False

    # Compute confidence on queue chunks.
    # NB: the following loop is skipped if there is a single chunk.
    for chunk_id in range(chunk_id_start, min(queue_length, n_chunks - 1)):
        # Print chunk ID and number of chunks.
        logger.info(
            "Chunk ID: {}/{}".format(
                str(1 + chunk_id).zfill(len(str(n_chunks))), n_chunks
            )
        )

        # Predict.
        chunk_pcen = deque[chunk_id]
        if has_context:
            chunk_confidence = predict_with_context(
                chunk_pcen, deque_context, detector, logger_level, padding=chunk_padding
            )
        else:
            chunk_confidence = predict(
                chunk_pcen, detector, logger_level, padding=chunk_padding
            )

        # Map confidence to 0-100 range.
        chunk_confidence = map_confidence(chunk_confidence, detector_name)

        # If continuous confidence is required, store it in memory.
        if export_confidence:
            chunk_confidences.append(chunk_confidence)

        # If thresholding is not required, jump to next chunk.
        if threshold is None:
            continue

        # Find peaks.
        peak_locs, _ = scipy.signal.find_peaks(chunk_confidence, height=min_peak_height)
        peak_vals = chunk_confidence[peak_locs]

        # Threshold peaks.
        th_peak_locs = peak_locs[peak_vals > threshold]
        th_peak_confidences = chunk_confidence[th_peak_locs]
        chunk_offset = chunk_duration * chunk_id
        chunk_timestamps = chunk_offset + th_peak_locs / frame_rate

        # Classify species.
        rows = []
        none_peak_ids = []
        for peak_id, th_peak_loc in enumerate(th_peak_locs):
            consistent_pred_dict, json_dict = classify_species(
                classifier, chunk_pcen, th_peak_loc, taxonomy
            )
            if consistent_pred_dict is None:
                none_peak_ids.append(peak_id)
                continue
            rows.append(
                {
                    "Order": consistent_pred_dict["coarse"]["scientific_name"],
                    "Order confidence (%)": consistent_pred_dict["coarse"]["probability"],
                    "Family": consistent_pred_dict["medium"]["scientific_name"],
                    "Family confidence (%)": consistent_pred_dict["medium"]["probability"],
                    "Species (English name)": consistent_pred_dict["fine"]["common_name"],
                    "Species (scientific name)": consistent_pred_dict["fine"]["scientific_name"],
                    "Species (4-letter code)": consistent_pred_dict["fine"]["taxonomy_level_aliases"]["species_4letter_code"],
                    "Species confidence (%)": consistent_pred_dict["fine"]["probability"],
                }
            )
            if predict_proba:
                chunk_timestamp = chunk_timestamps[peak_id]
                json_dict["Time (s)"] = float(chunk_timestamp)
                json_dict["Time (hh:mm:ss)"] = seconds_to_hhmmss(chunk_timestamp)
                json_dict["Detection confidence (%)"] = float(
                    th_peak_confidences[peak_id]
                )
                json_dicts.append(json_dict)
        th_peak_confidences = [
            th_peak_confidences[peak_id]
            for peak_id in range(len(th_peak_locs))
            if peak_id not in none_peak_ids
        ]
        chunk_timestamps = [
            chunk_timestamps[peak_id]
            for peak_id in range(len(th_peak_locs))
            if peak_id not in none_peak_ids
        ]
        n_peaks = len(chunk_timestamps)
        chunk_df = pd.DataFrame(rows, columns=df_columns)

        # Count flight calls.
        if n_peaks > 0:
            chunk_counter = collections.Counter(chunk_df["Species (English name)"])
            logger.info("Number of flight calls in current chunk: {}".format(n_peaks))
            logger.info(
                "("
                + ", ".join(
                    (str(v) + " " + k) for (k, v) in chunk_counter.most_common()
                )
                + ")"
            )
        else:
            logger.info("Number of flight calls in current chunk: 0")
        logger.info("")

        # Export checklist.
        chunk_hhmmss = list(map(seconds_to_hhmmss, chunk_timestamps))
        chunk_df["Time (hh:mm:ss)"] = event_hhmmss + chunk_hhmmss
        chunk_df["Detection confidence (%)"] = th_peak_confidences
        df_columns = [
            column
            for column in [
                "Time (hh:mm:ss)",
                "Detection confidence (%)",
                "Order",
                "Order confidence (%)",
                "Family",
                "Family confidence (%)",
                "Species (English name)",
                "Species (scientific name)",
                "Species (4-letter code)",
                "Species confidence (%)",
            ]
            if column in chunk_df
        ]
        df = df.append(chunk_df)
        df.to_csv(
            checklist_path, columns=df_columns,
            index=False, float_format="%.2f%%"
        )

        # Export probabilities as JSON file.
        if predict_proba:
            with open(json_path, "w") as f:
                json.dump(
                    {
                        "events": json_dicts,
                        "metadata": json_metadata,
                        "taxonomy": taxonomy,
                    },
                    f,
                )

        # Export clips.
        if export_clips and len(df) > 0:
            chunk_zip = zip(
                chunk_timestamps,
                chunk_hhmmss,
                list(th_peak_confidences),
                list(df["Species (4-letter code)"]),
            )
            for (
                clip_timestamp,
                clip_hhmmss,
                clip_confidence,
                clip_4lettercode,
            ) in chunk_zip:
                clip_start = max(
                    0, int(np.round(sr * (clip_timestamp - 0.5 * clip_duration)))
                )
                clip_stop = min(
                    len(sound_file),
                    int(np.round(sr * (clip_timestamp + 0.5 * clip_duration))),
                )
                sound_file.seek(clip_start)
                audio_clip = sound_file.read(clip_stop - clip_start)
                clip_hhmmss_escaped = clip_hhmmss.replace(":", "_").replace(".", "-")
                clip_name = suffix + "_".join(
                    [clip_hhmmss_escaped, str(int(clip_confidence)), clip_4lettercode]
                )
                clip_path = get_output_path(
                    filepath, clip_name + ".wav", output_dir=clips_dir
                )
                sf.write(clip_path, audio_clip, sr)

    # Loop over chunks.
    chunk_id = queue_length
    while chunk_id < (n_chunks - 1):
        # Print chunk ID and number of chunks.
        logger.info(
            "Chunk ID: {}/{}".format(
                str(1 + chunk_id).zfill(len(str(n_chunks))), n_chunks
            )
        )

        # Read chunk.
        chunk_start = chunk_id * chunk_length
        sound_file.seek(chunk_start)
        chunk_audio = sound_file.read(chunk_length + chunk_padding)

        # Compute PCEN.
        deque.popleft()
        chunk_pcen = compute_pcen(chunk_audio, sr)
        deque.append(chunk_pcen)

        # Compute percentiles
        concat_deque = np.concatenate(deque, axis=1, out=concat_deque)
        deque_context = np.percentile(
            concat_deque, percentiles, axis=1, out=deque_context, overwrite_input=True
        )

        # Append context.
        if export_context:
            contexts.append(deque_context)

        # Compute sensor fault features.
        # Median is 4th order statistic. Restrict to lowest 120 mel-freq bins
        context_median = deque_context[4, :120]
        context_median_medfilt = scipy.signal.medfilt(context_median, kernel_size=(13,))
        sensorfault_features = context_median_medfilt[::12].reshape(1, -1)

        # Compute probability of sensor fault.
        sensor_fault_probability = sensorfault_model.predict_proba(
            sensorfault_features
        )[0][1]

        # Add row to sensor fault log.
        faultlist_df = faultlist_df.append(
            {
                "Start (hh:mm:ss)": seconds_to_hhmmss(chunk_id * chunk_duration),
                "Stop (hh:mm:ss)": seconds_to_hhmmss((chunk_id + 1) * chunk_duration),
                "Fault confidence (%)": int(sensor_fault_probability * 100),
            },
            ignore_index=True,
        )
        if export_faults:
            faultlist_df.to_csv(
                faultlist_path, columns=faultlist_df_columns,
                index=False, float_format="%.2f%%"
            )

        # If probability of sensor fault is above threshold, exclude chunk.
        has_sensor_fault = sensor_fault_probability > bva_threshold
        if has_sensor_fault:
            logger.info(
                "Probability of sensor fault: {:5.2f}%".format(
                    100 * sensor_fault_probability
                )
            )
            context_duration = queue_length * chunk_duration
            ignored_start_str = str(
                datetime.timedelta(seconds=chunk_id * chunk_duration)
            )
            ignored_stop_str = str(
                datetime.timedelta(seconds=(chunk_id + 1) * chunk_duration)
            )
            logger.info(
                "Ignoring segment between "
                + ignored_start_str
                + " and "
                + ignored_stop_str
                + " (1 chunk)"
            )
            if export_confidence:
                chunk_confidence_length = int(
                    queue_length * chunk_duration * frame_rate
                )
                chunk_confidences.append(np.full(chunk_confidence_length, np.nan))
            chunk_id = chunk_id + 1
            continue

        # Otherwise, detect flight calls.
        if has_context:
            chunk_confidence = predict_with_context(
                chunk_pcen, deque_context, detector, logger_level, padding=chunk_padding
            )
        else:
            chunk_confidence = predict(
                chunk_pcen, detector, logger_level, padding=chunk_padding
            )

        # Map confidence to 0-100 range.
        chunk_confidence = map_confidence(chunk_confidence, detector_name)

        # If continuous confidence is required, store it in memory.
        if export_confidence:
            chunk_confidences.append(chunk_confidence)

        # If thresholding is not required, jump to next chunk.
        if threshold is None:
            continue

        # Find peaks.
        peak_locs, _ = scipy.signal.find_peaks(chunk_confidence, height=min_peak_height)
        peak_vals = chunk_confidence[peak_locs]

        # Threshold peaks.
        th_peak_locs = peak_locs[peak_vals > threshold]
        th_peak_confidences = chunk_confidence[th_peak_locs]
        chunk_offset = chunk_duration * chunk_id
        chunk_timestamps = chunk_offset + th_peak_locs / frame_rate

        # Classify species.
        rows = []
        none_peak_ids = []
        for peak_id, th_peak_loc in enumerate(th_peak_locs):
            consistent_pred_dict, json_dict = classify_species(
                classifier, chunk_pcen, th_peak_loc, taxonomy
            )
            if consistent_pred_dict is None:
                none_peak_ids.append(peak_id)
                continue
            rows.append(
                {
                    "Order": consistent_pred_dict["coarse"]["scientific_name"],
                    "Order confidence (%)": consistent_pred_dict["coarse"]["probability"],
                    "Family": consistent_pred_dict["medium"]["scientific_name"],
                    "Family confidence (%)": consistent_pred_dict["medium"]["probability"],
                    "Species (English name)": consistent_pred_dict["fine"]["common_name"],
                    "Species (scientific name)": consistent_pred_dict["fine"]["scientific_name"],
                    "Species (4-letter code)": consistent_pred_dict["fine"]["taxonomy_level_aliases"]["species_4letter_code"],
                    "Species confidence (%)": consistent_pred_dict["fine"]["probability"],
                }
            )
            if predict_proba:
                chunk_timestamp = chunk_timestamps[peak_id]
                json_dict["Time (s)"] = float(chunk_timestamp)
                json_dict["Time (hh:mm:ss)"] = seconds_to_hhmmss(chunk_timestamp)
                json_dict["Detection confidence (%)"] = float(
                    th_peak_confidences[peak_id]
                )
                json_dicts.append(json_dict)
        th_peak_confidences = [
            th_peak_confidences[peak_id]
            for peak_id in range(len(th_peak_locs))
            if peak_id not in none_peak_ids
        ]
        chunk_timestamps = [
            chunk_timestamps[peak_id]
            for peak_id in range(len(th_peak_locs))
            if peak_id not in none_peak_ids
        ]
        n_peaks = len(chunk_timestamps)
        chunk_df = pd.DataFrame(rows, columns=df_columns)

        # Count flight calls.
        if n_peaks > 0:
            chunk_counter = collections.Counter(chunk_df["Species (English name)"])
            logger.info("Number of flight calls in current chunk: {}".format(n_peaks))
            logger.info(
                "("
                + ", ".join(
                    (str(v) + " " + k) for (k, v) in chunk_counter.most_common()
                )
                + ")"
            )
        else:
            logger.info("Number of flight calls in current chunk: 0")
        logger.info("")

        # Export checklist.
        chunk_hhmmss = list(map(seconds_to_hhmmss, chunk_timestamps))
        chunk_df["Time (hh:mm:ss)"] = event_hhmmss + chunk_hhmmss
        chunk_df["Detection confidence (%)"] = th_peak_confidences
        df_columns = [
            column
            for column in [
                "Time (hh:mm:ss)",
                "Detection confidence (%)",
                "Order",
                "Order confidence (%)",
                "Family",
                "Family confidence (%)",
                "Species (English name)",
                "Species (scientific name)",
                "Species (4-letter code)",
                "Species confidence (%)",
            ]
            if column in chunk_df
        ]
        df = df.append(chunk_df)
        df.to_csv(
            checklist_path, columns=df_columns,
            index=False, float_format="%.2f%%"
        )

        # Export probabilities as JSON file.
        if predict_proba:
            with open(json_path, "w") as f:
                json.dump(
                    {
                        "events": json_dicts,
                        "metadata": json_metadata,
                        "taxonomy": taxonomy,
                    },
                    f,
                )

        # Export clips.
        if export_clips and len(df) > 0:
            chunk_zip = zip(
                chunk_timestamps,
                chunk_hhmmss,
                list(th_peak_confidences),
                list(df["Species (4-letter code)"]),
            )
            for (
                clip_timestamp,
                clip_hhmmss,
                clip_confidence,
                clip_4lettercode,
            ) in chunk_zip:
                clip_start = max(
                    0, int(np.round(sr * (clip_timestamp - 0.5 * clip_duration)))
                )
                clip_stop = min(
                    len(sound_file),
                    int(np.round(sr * (clip_timestamp + 0.5 * clip_duration))),
                )
                sound_file.seek(clip_start)
                audio_clip = sound_file.read(clip_stop - clip_start)
                clip_hhmmss_escaped = clip_hhmmss.replace(":", "_").replace(".", "-")
                clip_name = suffix + "_".join(
                    [clip_hhmmss_escaped, str(int(clip_confidence)), clip_4lettercode]
                )
                clip_path = get_output_path(
                    filepath, clip_name + ".wav", output_dir=clips_dir
                )
                sf.write(clip_path, audio_clip, sr)

        # Go to next chunk.
        chunk_id = chunk_id + 1

    # Last chunk. For n_chunks>1, we reuse the context from the penultimate
    # chunk because this last chunk is typically shorter than chunk_length.
    # But if the queue is empty (n_chunks==1), we compute context on the fly
    # even if this chunk is shorter. This can potentially be numerically
    # unstable with files shorter than 30 minutes, which is why we issue a
    # warning. Also, we do not try to detect sensor faults in files shorter than
    # 30 minutes.
    if n_chunks > 1:
        faultlist_df = faultlist_df.append(
            {
                "Start (hh:mm:ss)": seconds_to_hhmmss(chunk_id * chunk_duration),
                "Stop (hh:mm:ss)": seconds_to_hhmmss(full_length / sr),
                "Fault confidence (%)": int(sensor_fault_probability * 100),
            },
            ignore_index=True,
        )
        if export_faults:
            faultlist_df.to_csv(
                faultlist_path, columns=faultlist_df_columns,
                index=False, float_format="%.2f%%"
            )

    if (n_chunks > 1) and has_sensor_fault:
        logger.info(
            "Probability of sensor fault: {:5.2f}%".format(
                100 * sensor_fault_probability
            )
        )
        ignored_start_str = str(datetime.timedelta(seconds=chunk_id * chunk_duration))
        ignored_stop_str = str(datetime.timedelta(seconds=full_length / sr))
        logger.info(
            "Ignoring segment between "
            + ignored_start_str
            + " and "
            + ignored_stop_str
            + " (i.e., up to end of file)"
        )
    else:
        logger.info("Chunk ID: {}/{}".format(n_chunks, n_chunks))
        chunk_start = (n_chunks - 1) * chunk_length
        sound_file.seek(chunk_start)
        context_duration = queue_length * chunk_duration
        chunk_audio = sound_file.read(full_length - chunk_start)
        chunk_pcen = compute_pcen(chunk_audio, sr)
        chunk_confidence_length = int(frame_rate * full_length / sr)
        chunk_confidence = np.full(chunk_confidence_length, np.nan)

        if (export_context or has_context) and (n_chunks == 1):
            deque_context = np.percentile(
                chunk_pcen, percentiles, axis=1, overwrite_input=True
            )
            logging.warning(
                "\nFile duration ("
                + str(datetime.timedelta(seconds=full_length / sr))
                + ") shorter than 25% of context duration ("
                + str(datetime.timedelta(seconds=context_duration))
                + ").\n"
                "This may cause numerical instabilities in threshold adaptation.\n"
                + "We recommend disabling the context-adaptive threshold\n"
                + "(i.e., setting 'detector_name'='birdvoxdetect-v03_trial-12_network_epoch-06') when\n"
                + "running birdvoxdetect on short audio files."
            )
            has_sensor_fault = False
        elif has_context:
            # Compute percentiles
            deque.popleft()
            deque.append(chunk_pcen)
            concat_deque = np.concatenate(deque, axis=1)
            deque_context = np.percentile(
                concat_deque,
                percentiles,
                axis=1,
                out=deque_context,
                overwrite_input=True,
            )

        # Append context.
        if export_context:
            contexts.append(deque_context)

    if not has_sensor_fault:
        # Define trimming length for last chunk.
        # This one is not equal to one second but to the duration
        # of a BVD/BVC clip, i.e. about 150 milliseconds.
        # Note that this trimming is not compensated by the presence of
        # the next chunk because we are already at the last chunk.
        # In other words, if a flight call happens at the last 150 milliseconds
        # of an audio recording, it is ignored.
        lastchunk_trimming = BVD_CLIP_LENGTH * pcen_settings["hop_length"]
        if has_context:
            # Predict.
            chunk_confidence = predict_with_context(
                chunk_pcen,
                deque_context,
                detector,
                logger_level,
                padding=lastchunk_trimming,
            )
        else:
            # Predict.
            chunk_confidence = predict(
                chunk_pcen, detector, logger_level, padding=lastchunk_trimming
            )

        # Map confidence to 0-100 range.
        chunk_confidence = map_confidence(chunk_confidence, detector_name)

        # Threshold last chunk if required.
        if threshold is not None:

            # Find peaks.
            peak_locs, _ = scipy.signal.find_peaks(
                chunk_confidence, height=min_peak_height
            )
            peak_vals = chunk_confidence[peak_locs]

            # Threshold peaks.
            th_peak_locs = peak_locs[peak_vals > threshold]
            th_peak_confidences = chunk_confidence[th_peak_locs]
            chunk_offset = chunk_duration * (n_chunks - 1)
            chunk_timestamps = chunk_offset + th_peak_locs / frame_rate

            # Classify species.
            rows = []
            none_peak_ids = []
            for peak_id, th_peak_loc in enumerate(th_peak_locs):
                consistent_pred_dict, json_dict = classify_species(
                    classifier, chunk_pcen, th_peak_loc, taxonomy
                )
                if consistent_pred_dict is None:
                    none_peak_ids.append(peak_id)
                    continue
                rows.append(
                    {
                        "Order": consistent_pred_dict["coarse"]["scientific_name"],
                        "Order confidence (%)": consistent_pred_dict["coarse"]["probability"],
                        "Family": consistent_pred_dict["medium"]["scientific_name"],
                        "Family confidence (%)": consistent_pred_dict["medium"]["probability"],
                        "Species (English name)": consistent_pred_dict["fine"]["common_name"],
                        "Species (scientific name)": consistent_pred_dict["fine"]["scientific_name"],
                        "Species (4-letter code)": consistent_pred_dict["fine"]["taxonomy_level_aliases"]["species_4letter_code"],
                        "Species confidence (%)": consistent_pred_dict["fine"]["probability"],
                    }
                )
                if predict_proba:
                    chunk_timestamp = chunk_timestamps[peak_id]
                    json_dict["Time (s)"] = float(chunk_timestamp)
                    json_dict["Time (hh:mm:ss)"] = seconds_to_hhmmss(chunk_timestamp)
                    json_dict["Detection confidence (%)"] = float(
                        th_peak_confidences[peak_id]
                    )
                    json_dicts.append(json_dict)
            th_peak_confidences = [
                th_peak_confidences[peak_id]
                for peak_id in range(len(th_peak_locs))
                if peak_id not in none_peak_ids
            ]
            chunk_timestamps = [
                chunk_timestamps[peak_id]
                for peak_id in range(len(th_peak_locs))
                if peak_id not in none_peak_ids
            ]
            n_peaks = len(chunk_timestamps)
            chunk_df = pd.DataFrame(rows, columns=df_columns)

            # Count flight calls.
            if n_peaks > 0:
                chunk_counter = collections.Counter(chunk_df["Species (English name)"])
                logger.info(
                    "Number of flight calls in current chunk: {}".format(n_peaks)
                )
                logger.info(
                    "("
                    + ", ".join(
                        (str(v) + " " + k) for (k, v) in chunk_counter.most_common()
                    )
                    + ")"
                )
            else:
                logger.info("Number of flight calls in current chunk: 0")
            logger.info("")

            # Export checklist.
            chunk_hhmmss = list(map(seconds_to_hhmmss, chunk_timestamps))
            chunk_df["Time (hh:mm:ss)"] = event_hhmmss + chunk_hhmmss
            chunk_df["Detection confidence (%)"] = th_peak_confidences
            df_columns = [
                column
                for column in [
                    "Time (hh:mm:ss)",
                    "Detection confidence (%)",
                    "Order",
                    "Order confidence (%)",
                    "Family",
                    "Family confidence (%)",
                    "Species (English name)",
                    "Species (scientific name)",
                    "Species (4-letter code)",
                    "Species confidence (%)",
                ]
                if column in chunk_df
            ]
            df = df.append(chunk_df)
            df.to_csv(
                checklist_path, columns=df_columns,
                index=False, float_format='%.2f%%'
            )

            # Export probabilities as JSON file.
            if predict_proba:
                with open(json_path, "w") as f:
                    json_faultlist = faultlist_df.to_json(orient="index")
                    json_metadata["elapsed_time"] = time.time() - start_time
                    json.dump(
                        {
                            "events": json_dicts,
                            "metadata": json_metadata,
                            "sensor_faults": json.loads(json_faultlist),
                            "taxonomy": taxonomy,
                        },
                        f,
                    )

            # Export clips.
            if export_clips and len(df) > 0:
                chunk_zip = zip(
                    chunk_timestamps,
                    chunk_hhmmss,
                    list(th_peak_confidences),
                    list(df["Species (4-letter code)"]),
                )
                for (
                    clip_timestamp,
                    clip_hhmmss,
                    clip_confidence,
                    clip_4lettercode,
                ) in chunk_zip:
                    clip_start = max(
                        0, int(np.round(sr * (clip_timestamp - 0.5 * clip_duration)))
                    )
                    clip_stop = min(
                        len(sound_file),
                        int(np.round(sr * (clip_timestamp + 0.5 * clip_duration))),
                    )
                    sound_file.seek(clip_start)
                    audio_clip = sound_file.read(clip_stop - clip_start)
                    clip_hhmmss_escaped = clip_hhmmss.replace(":", "_").replace(
                        ".", "-"
                    )
                    clip_name = suffix + "_".join(
                        [
                            clip_hhmmss_escaped,
                            str(int(clip_confidence)),
                            clip_4lettercode,
                        ]
                    )
                    clip_path = get_output_path(
                        filepath, clip_name + ".wav", output_dir=clips_dir
                    )
                    sf.write(clip_path, audio_clip, sr)

    # Export confidence curve.
    if export_confidence:

        # Define output path for confidence.
        confidence_path = get_output_path(
            filepath, suffix + "confidence.hdf5", output_dir=output_dir
        )

        # Export confidence curve, chunk by chunk.
        # NB: looping over chunks, rather than concatenating them into a single
        # NumPy array, guarantees that this export has a O(1) memory footprint
        # with respect to the duration of the input file. As a result,
        # BirdVoxDetect is guaranteed to run with 3-4 gigabytes of RAM
        # for a context duration of 30 minutes, whatever be the duration
        # of the input.
        chunk_confidences.append(chunk_confidence)
        total_length = sum(map(len, chunk_confidences))
        with h5py.File(confidence_path, "w") as f:
            f.create_dataset("confidence", (total_length,), dtype="float32")
            f.create_dataset("time", (total_length,), dtype="float32")
            f["chunk_duration"] = chunk_duration
            f["frame_rate"] = frame_rate

        chunk_pointer = 0

        # Loop over chunks.
        for chunk_id, chunk_confidence in enumerate(chunk_confidences):

            # Define offset.
            chunk_length = len(chunk_confidence)
            next_chunk_pointer = chunk_pointer + chunk_length
            chunk_start = chunk_duration * chunk_id
            chunk_stop = chunk_start + chunk_length / frame_rate
            chunk_time = np.linspace(
                chunk_start,
                chunk_stop,
                num=chunk_length,
                endpoint=False,
                dtype=np.float32,
            )

            # Export chunk as HDF5
            with h5py.File(confidence_path, "a") as f:
                f["confidence"][chunk_pointer:next_chunk_pointer] = chunk_confidence
                f["time"][chunk_pointer:next_chunk_pointer] = chunk_time

            # Increment pointer.
            chunk_pointer = next_chunk_pointer

    # Export context.
    if export_context:
        # Define output path for context.
        context_path = get_output_path(
            filepath, suffix + "context.hdf5", output_dir=output_dir
        )

        # Stack context over time.
        context_array = np.stack(contexts, axis=-1)

        # Export context.
        with h5py.File(context_path, "w") as f:
            f["context"] = context_array
            f["chunk_duration"] = chunk_duration
            f["frame_rate"] = frame_rate

    # Print final messages.
    if threshold is not None:
        df = pd.read_csv(checklist_path)
        if (len(df) > 0) and ("Species (English name)" in df.columns):
            logger.info(
                "\n".join(
                    [
                        (k + " " + str(v).rjust(6))
                        for (k, v) in collections.Counter(df["Species (English name)"]).most_common()
                    ]
                )
            )
            logger.info("TOTAL: {}.".format(str(len(df)).rjust(4)))
        else:
            logger.info("TOTAL: 0. No flight calls were detected.")
        timestamp_str = "Checklist is available at: {}"
        logger.info(timestamp_str.format(checklist_path))
    if export_clips:
        logger.info("Clips are available at: {}".format(clips_dir))
    if export_confidence:
        event_str = "The event detection curve is available at: {}"
        logger.info(event_str.format(confidence_path))
    if export_context:
        context_str = "The context array is available at: {}"
        logger.info(context_str.format(context_path))
    if export_faults:
        faultlist_str = "The list of sensor faults is available at: {}"
        logger.info(faultlist_str.format(faultlist_path))
    if predict_proba:
        proba_str = "The list of probabilistic outputs is available at: {}"
        logger.info(proba_str.format(json_path))
    logger.info("Done with file: {}.".format(filepath))

    return df

def classify_species(classifier, chunk_pcen, th_peak_loc, taxonomy):
    # Load settings
    pcen_settings = get_pcen_settings()
    clip_length = BVD_CLIP_LENGTH

    # Convert birdvoxdetect hops to PCEN hops
    th_peak_hop = th_peak_loc * pcen_settings["stride_length"]

    # Extract clip in PCEN domain
    pcen_clip_start = th_peak_hop - clip_length // 2
    pcen_clip_stop = th_peak_hop + clip_length // 2
    pcen_clip = chunk_pcen[:120, pcen_clip_start:pcen_clip_stop, np.newaxis]

    # If PCEN clip is empty, return None
    if pcen_clip.shape[1] == 0:
        return None, None

    # Run BirdVoxClassify.
    bvc_prediction = birdvoxclassify.predict(pcen_clip, classifier=classifier)

    # Format prediction
    formatted_pred_dict = birdvoxclassify.format_pred(bvc_prediction, taxonomy=taxonomy)

    # Apply hierarchical consistency
    consistent_pred_dict = birdvoxclassify.get_best_candidates(
        formatted_pred_dict=formatted_pred_dict,
        taxonomy=taxonomy,
        hierarchical_consistency=True,
    )

    for level in consistent_pred_dict.keys():
        consistent_pred_dict[level]["probability"] *= 100

    if consistent_pred_dict["coarse"]["common_name"] == "other":
        consistent_pred_dict["coarse"]["common_name"] = ""
        consistent_pred_dict["coarse"]["scientific_name"] = ""

    if consistent_pred_dict["medium"]["common_name"] == "other":
        consistent_pred_dict["medium"]["common_name"] = ""
        consistent_pred_dict["medium"]["scientific_name"] = ""

    if consistent_pred_dict["fine"]["common_name"] == "other":
        consistent_pred_dict["fine"]["scientific_name"] = ""
        consistent_pred_dict["fine"]["taxonomy_level_aliases"] = {
            "species_4letter_code": "",
            "species_6letter_code": "",
        }

    return consistent_pred_dict, formatted_pred_dict

def compute_pcen(audio, sr):
    # Load settings.
    pcen_settings = get_pcen_settings()

    # Validate audio
    librosa.util.valid_audio(audio, mono=True)

    # Map to the range [-2**31, 2**31[
    audio = (audio * (2 ** 31)).astype("float32")

    # Resample to 22,050 Hz
    if not sr == pcen_settings["sr"]:
        audio = librosa.resample(audio, sr, pcen_settings["sr"])
        sr = pcen_settings["sr"]

    # Compute Short-Term Fourier Transform (STFT).
    stft = librosa.stft(
        audio,
        n_fft=pcen_settings["n_fft"],
        win_length=pcen_settings["win_length"],
        hop_length=pcen_settings["hop_length"],
        window=pcen_settings["window"],
    )

    # Compute squared magnitude coefficients.
    abs2_stft = (stft.real * stft.real) + (stft.imag * stft.imag)

    # Gather frequency bins according to the Mel scale.
    # NB: as of librosa v0.6.2, melspectrogram is type-instable and thus
    # returns 64-bit output even with a 32-bit input. Therefore, we need
    # to convert PCEN to single precision eventually. This might not be
    # necessary in the future, if the whole PCEN pipeline is kept type-stable.
    melspec = librosa.feature.melspectrogram(
        y=None,
        S=abs2_stft,
        sr=pcen_settings["sr"],
        n_fft=pcen_settings["n_fft"],
        n_mels=pcen_settings["n_mels"],
        htk=True,
        fmin=pcen_settings["fmin"],
        fmax=pcen_settings["fmax"],
    )

    # Compute PCEN.
    pcen = librosa.pcen(
        melspec,
        sr=pcen_settings["sr"],
        hop_length=pcen_settings["hop_length"],
        gain=pcen_settings["pcen_norm_exponent"],
        bias=pcen_settings["pcen_delta"],
        power=pcen_settings["pcen_power"],
        time_constant=pcen_settings["pcen_time_constant"],
    )

    # Convert to single floating-point precision.
    pcen = pcen.astype("float32")

    # Truncate spectrum to range 2-10 kHz.
    pcen = pcen[: pcen_settings["top_freq_id"], :]

    # Return.
    return pcen

def predict(pcen, detector, logger_level, padding=0):
    # Truncate frequency spectrum (from 128 to 120 bins)
    pcen = pcen[:120, :]
    pcen_settings = get_pcen_settings()

    # PCEN-SNR
    if detector == "pcen_snr":
        frame_rate = 20.0
        pcen_snr = np.max(pcen, axis=0) - np.min(pcen, axis=0)
        pcen_confidence = pcen_snr / (0.001 + pcen_snr)
        median_confidence = scipy.signal.medfilt(pcen_confidence, kernel_size=127)
        sr = pcen_settings["sr"]
        hop_length = pcen_settings["hop_length"]
        audio_duration = pcen.shape[1] * hop_length / sr
        confidence_x = np.arange(
            0.0, audio_duration * sr / hop_length, sr / (hop_length * frame_rate)
        )[:-1].astype("int")
        y = 100 * np.clip(median_confidence[confidence_x], 0.0, 1.0)

    # PCEN-CNN. (no context adaptation)
    else:
        # Compute number of hops.
        clip_length = BVD_CLIP_LENGTH
        pcen_settings = get_pcen_settings()
        stride_length = pcen_settings["stride_length"]
        n_freqs, n_padded_hops = pcen.shape
        if padding > 0:
            padding_hops = int(padding / pcen_settings["hop_length"])
            n_hops = n_padded_hops - padding_hops + 1
            n_strides = int(n_hops / stride_length)
        else:
            n_hops = n_padded_hops
            n_strides = int((n_hops - clip_length) / stride_length)
        itemsize = pcen.itemsize

        # Stride and tile.
        X_shape = (n_strides, clip_length, n_freqs)
        X_stride = (itemsize * n_freqs * stride_length, itemsize * n_freqs, itemsize)
        X_pcen = np.lib.stride_tricks.as_strided(
            np.ravel(map_tfr(pcen).T), shape=X_shape, strides=X_stride, writeable=False
        )
        X_pcen = np.transpose(X_pcen, (0, 2, 1))[:, :, :, np.newaxis]

        # Predict.
        verbose = logger_level < 15
        y = detector.predict(X_pcen, verbose=verbose)

    return np.squeeze(y)

def predict_with_context(pcen, context, detector, logger_level, padding=0):
    # Truncate frequency spectrum (from 128 to 120 bins)
    pcen = pcen[:120, :]
    context = context[:, :120]

    # Compute number of hops.
    clip_length = BVD_CLIP_LENGTH
    pcen_settings = get_pcen_settings()
    stride_length = pcen_settings["stride_length"]
    n_freqs, n_padded_hops = pcen.shape
    if padding > 0:
        padding_hops = int(padding / pcen_settings["hop_length"])
        n_hops = n_padded_hops - padding_hops + 1
        n_strides = int(n_hops / stride_length)
    else:
        n_hops = n_padded_hops
        n_strides = int((n_hops - clip_length) / stride_length)
    itemsize = pcen.itemsize

    # Stride and tile.
    X_shape = (n_strides, clip_length, n_freqs)
    X_stride = (itemsize * n_freqs * stride_length, itemsize * n_freqs, itemsize)
    X_pcen = np.lib.stride_tricks.as_strided(
        np.ravel(map_tfr(pcen).T), shape=X_shape, strides=X_stride, writeable=False
    )
    X_pcen = np.transpose(X_pcen, (0, 2, 1))[:, :, :, np.newaxis]
    X_bg = np.broadcast_to(
        context.T[np.newaxis, :, :], (n_strides, context.shape[1], context.shape[0])
    )

    # Predict.
    verbose = logger_level < 15
    y = detector.predict({"spec_input": X_pcen, "bg_input": X_bg}, verbose=verbose)

    # Return confidence.
    return y.squeeze()

def get_output_path(filepath, suffix, output_dir=None):
    """
    Parameters
    ----------
    filepath : str
        Path to audio file to be processed
    suffix : str
        String to append to filename (including extension)
    output_dir : str or None
        Path to directory where file will be saved.
        If None, will use directory of given filepath.
    Returns
    -------
    output_path : str
        Path to output file
    """
    base_filename = os.path.splitext(os.path.basename(filepath))[0]
    if not output_dir:
        output_dir = os.path.dirname(filepath)

    if suffix[0] != ".":
        output_filename = "{}_{}".format(base_filename, suffix)
    else:
        output_filename = base_filename + suffix

    return os.path.join(output_dir, output_filename)

def get_pcen_settings():
    pcen_settings = {
        "fmin": 2000.0,
        "fmax": 11025.0,
        "hop_length": 32,
        "n_fft": 1024,
        "n_mels": 128,
        "pcen_delta": 10.0,
        "pcen_time_constant": 0.06,
        "pcen_norm_exponent": 0.8,
        "pcen_power": 0.25,
        "sr": 22050.0,
        "stride_length": 34,
        "top_freq_id": 128,
        "win_length": 256,
        "window": "flattop",
    }
    return pcen_settings

def get_model_path(model_name):
    return os.path.join(os.path.dirname(__file__), "models", model_name)

def map_confidence(y, model_name):
    log1my = np.log1p(np.clip(-y, np.finfo(np.float32).eps - 1, None))
    logy = np.log(np.clip(y, np.finfo(np.float32).tiny, None))
    y_inverse_sigmoid = log1my - logy - np.log(np.finfo(np.float32).eps)
    if model_name == "birdvoxdetect-v03_trial-12_network_epoch-068":
        # See birdvox-full-season/detector-v03/notebooks/07_measure-precision-300h.ipynb
        y_in = np.maximum(0, y_inverse_sigmoid - 18) ** 2 / 100
        y_out = (
            14.76561354 * (y_in ** 3)
            - 68.54604756 * (y_in ** 2)
            + 111.89379155 * (y_in)
            - 0.13061346
        )
    elif model_name == "birdvoxdetect-v03_T-1800_trial-37_network_epoch-023":
        # See birdvox-full-season/detector-v03/notebooks/10_measure-precision-300h-ca.ipynb
        y_in = np.maximum(0, y_inverse_sigmoid - 18) ** 2 / 100
        y_out = (
            4.28734484 * (y_in ** 3)
            - 25.97219728 * (y_in ** 2)
            + 62.66749547 * (y_in)
            + 4.8942351
        )
    else:
        y_in = y_inverse_sigmoid ** 2
        y_out = 0.09 * y_in
    return np.clip(y_out, 0.0, 99.99)

def seconds_to_hhmmss(total_seconds):
    hours, tmp_seconds = divmod(total_seconds, 3600)
    minutes, seconds = divmod(tmp_seconds, 60)
    return "{:02d}:{:02d}:{:05.2f}".format(int(hours), int(minutes), seconds)


from bs4 import BeautifulSoup as bs
import numpy as np
import sys
from urllib import request as req
import re

base_url = 'https://webbook.nist.gov/cgi/cbook.cgi?ID='
gas_base_url = 'https://webbook.nist.gov'

def contain_gas_chrom(tag):
  '''
  custom search gas chromatography link for beautiful soup
  '''
  if tag.name == 'a' and '#Gas-Chrom' in tag.attrs['href']:
    return True
  else:
    return False

def get_gasChrom_url(cas_number):
  url = base_url + cas_number
  # print (url)
  page = req.urlopen(url)
  soup = bs(page, 'html.parser')
  try:
    gas_chrom_href = soup.find(contain_gas_chrom).attrs['href']
  except:
    gas_chrom_href = None
  # print (gas_chrom_href)

  return gas_chrom_href

def get_ri(cas_number, column_type):
  '''
    Get average RI from nistwebbook
    cas_number:  CAS number of the compound
    comlumn_type: '5ms' or 'wax'
  '''
  try:
    gas_url = gas_base_url + get_gasChrom_url(cas_number)
    page = req.urlopen(gas_url)
  except:
    return -1

  gas_soup = bs(page, 'html.parser')
  exp_rows = gas_soup.find_all('tr', class_='exp')

  RIs = []
  if column_type == '5ms':
    reg = '-5\Z|-5[^0-9]'
  elif column_type == 'wax':
    reg = '(wax)'
  for row in exp_rows:
    tds = row.find_all('td')
    column =tds[1].text
    match = re.search(reg, column, re.IGNORECASE)
    if not match:
      continue
    # print (column)
    # print (tds)
    ri = float(tds[2].text)
    if ri > 600:
      RIs.append(ri)

  RI = -1
  if len(RIs) == 0:
    return -1
  else:
    return np.mean(RIs)

def main():
    '''
    Returns average retention index from nist webbook for 5ms or wax column
    usage:  lookup_ri casnumber 5ms or lookup_ri casnumber wax
    '''
    if len(sys.argv) != 3:
        print ('Usage: lookup_ri casNumber 5ms or lookup_ri casNumber wax')
        return 0

    cas = sys.argv[1]
    column = sys.argv[2]
    # print (cas, column)
    ri = get_ri(cas, column)
    print (ri)
    return

if __name__ == '__main__':
    main()


import argparse
import math
import os
import pickle
import time
from collections import deque
from random import randint

import numpy as np
import pygame
from enum import IntEnum
from pygame.locals import *
from scipy.ndimage.filters import gaussian_filter

########################################################################################################################
parser = argparse.ArgumentParser(description='Q-Learn how to play Flappy Bird')
parser.add_argument('--actor_file', type=str, default='', help='file path to the actor file')
parser.add_argument('--save_file', type=str, default='actor.pickle',
                    help='file path which you would like to write to (default is actor.pickle)')
parser.add_argument('--speed', type=int, default=1, help='game speed (default 1)')
parser.add_argument('--skip_frames', type=int, default=1,
                    help='only allow the bird to make decisions on multiples of this frame')

parser.add_argument('--number_of_birds', type=int, default=1,
                    help='number of birds to play flappy bird with')
parser.add_argument('--random_start_point', type=bool, default=False,
                    help='use random starting point for bird(s)')

parser.add_argument('--alpha', type=float, default=.9, help='learning rate (0.0 < alpha < 1.0)')
parser.add_argument('--blur_freq', type=int, default=10,
                    help='the number of runs between each gaussian blur')

parser.add_argument('--sigma', type=float, default=2.0, help='sigma for guassian blur')

parser.add_argument('--bias', type=bool, default=True,
                    help='introduce bias in the event of ties to help the bird learn faster')
parser.add_argument('--intercede', type=bool, default=True,
                    help='do not allow the bird to suicide on floor/ceiling')

args = parser.parse_args()

ACTOR_FILE = args.actor_file
SAVE_FILE = args.save_file

OVERALL_SPEED = args.speed
SKIP_FRAMES = args.skip_frames

NUMBER_OF_BIRDS = args.number_of_birds
RANDOM_START_POINT = args.random_start_point

ALPHA = args.alpha
SIGMA = args.blur_freq

BLUR_FREQ = args.blur_freq

BIAS = args.bias
INTERCEDE = args.bias
########################################################################################################################

FPS = 60

ANIMATION_SPEED = 0.18 * args.speed  # pixels per millisecond
WIN_WIDTH = 284 * 2  # BG image size: 284x512 px; tiled twice
WIN_HEIGHT = 512

class Bird(pygame.sprite.Sprite):
    """Represents the bird controlled by the player.

    The bird is the 'hero' of this game.  The player can make it climb
    (ascend quickly), otherwise it sinks (descends more slowly).  It must
    pass through the space in between pipes (for every pipe passed, one
    point is scored); if it crashes into a pipe, the game ends.

    Attributes:
    x: The bird's X coordinate.
    y: The bird's Y coordinate.
    msec_to_climb: The number of milliseconds left to climb, where a
        complete climb lasts Bird.CLIMB_DURATION milliseconds.

    Constants:
    WIDTH: The width, in pixels, of the bird's image.
    HEIGHT: The height, in pixels, of the bird's image.
    SINK_SPEED: With which speed, in pixels per millisecond, the bird
        descends in one second while not climbing.
    CLIMB_SPEED: With which speed, in pixels per millisecond, the bird
        ascends in one second while climbing, on average.  See also the
        Bird.update docstring.
    CLIMB_DURATION: The number of milliseconds it takes the bird to
        execute a complete climb.
    """

    WIDTH = HEIGHT = 32
    SINK_SPEED = 0.18 * OVERALL_SPEED
    CLIMB_SPEED = 0.3 * OVERALL_SPEED
    CLIMB_DURATION = 333.3 / OVERALL_SPEED

    def __init__(self, x, y, msec_to_climb, images):
        """Initialise a new Bird instance.

        Arguments:
        x: The bird's initial X coordinate.
        y: The bird's initial Y coordinate.
        msec_to_climb: The number of milliseconds left to climb, where a
            complete climb lasts Bird.CLIMB_DURATION milliseconds.  Use
            this if you want the bird to make a (small?) climb at the
            very beginning of the game.
        images: A tuple containing the images used by this bird.  It
            must contain the following images, in the following order:
                0. image of the bird with its wing pointing upward
                1. image of the bird with its wing pointing downward
        """
        super(Bird, self).__init__()
        self.x, self.y = x, y
        self.msec_to_climb = msec_to_climb
        self._img_wingup, self._img_wingdown = images
        self._mask_wingup = pygame.mask.from_surface(self._img_wingup)
        self._mask_wingdown = pygame.mask.from_surface(self._img_wingdown)
        self.dead = False

    def update(self, delta_frames=1):
        """Update the bird's position.

        This function uses the cosine function to achieve a smooth climb:
        In the first and last few frames, the bird climbs very little, in the
        middle of the climb, it climbs a lot.
        One complete climb lasts CLIMB_DURATION milliseconds, during which
        the bird ascends with an average speed of CLIMB_SPEED px/ms.
        This Bird's msec_to_climb attribute will automatically be
        decreased accordingly if it was > 0 when this method was called.

        Arguments:
        delta_frames: The number of frames elapsed since this method was
            last called.
        """
        if self.msec_to_climb > 0:
            frac_climb_done = 1 - self.msec_to_climb / Bird.CLIMB_DURATION
            self.y -= (Bird.CLIMB_SPEED * frames_to_msec(delta_frames) *
                       (1 - math.cos(frac_climb_done * math.pi)))
            self.msec_to_climb -= frames_to_msec(delta_frames)
        else:
            self.y += Bird.SINK_SPEED * frames_to_msec(delta_frames)

    @property
    def image(self):
        """Get a Surface containing this bird's image.

        This will decide whether to return an image where the bird's
        visible wing is pointing upward or where it is pointing downward
        based on pygame.time.get_ticks().  This will animate the flapping
        bird, even though pygame doesn't support animated GIFs.
        """
        if pygame.time.get_ticks() % 500 >= 250:
            return self._img_wingup
        else:
            return self._img_wingdown

    @property
    def mask(self):
        """Get a bitmask for use in collision detection.

        The bitmask excludes all pixels in self.image with a
        transparency greater than 127."""
        if pygame.time.get_ticks() % 500 >= 250:
            return self._mask_wingup
        else:
            return self._mask_wingdown

    @property
    def rect(self):
        """Get the bird's position, width, and height, as a pygame.Rect."""
        return Rect(self.x, self.y, Bird.WIDTH, Bird.HEIGHT)

    def jump(self):
        self.msec_to_climb = Bird.CLIMB_DURATION

class PipePair(pygame.sprite.Sprite):
    """Represents an obstacle.

    A PipePair has a top and a bottom pipe, and only between them can
    the bird pass -- if it collides with either part, the game is over.

    Attributes:
    x: The PipePair's X position.  This is a float, to make movement
        smoother.  Note that there is no y attribute, as it will only
        ever be 0.
    image: A pygame.Surface which can be blitted to the display surface
        to display the PipePair.
    mask: A bitmask which excludes all pixels in self.image with a
        transparency greater than 127.  This can be used for collision
        detection.
    top_pieces: The number of pieces, including the end piece, in the
        top pipe.
    bottom_pieces: The number of pieces, including the end piece, in
        the bottom pipe.

    Constants:
    WIDTH: The width, in pixels, of a pipe piece.  Because a pipe is
        only one piece wide, this is also the width of a PipePair's
        image.
    PIECE_HEIGHT: The height, in pixels, of a pipe piece.
    ADD_INTERVAL: The interval, in milliseconds, in between adding new
        pipes.
    """

    WIDTH = 80
    PIECE_HEIGHT = 32
    ADD_INTERVAL = 3000 / OVERALL_SPEED

    def __init__(self, pipe_end_img, pipe_body_img):
        """Initialises a new random PipePair.

        The new PipePair will automatically be assigned an x attribute of
        float(WIN_WIDTH - 1).

        Arguments:
        pipe_end_img: The image to use to represent a pipe's end piece.
        pipe_body_img: The image to use to represent one horizontal slice
            of a pipe's body.
        """

        self.passed = set()

        self.x = float(WIN_WIDTH - 1)
        self.score_counted = set()

        self.image = pygame.Surface((PipePair.WIDTH, WIN_HEIGHT), SRCALPHA)
        self.image.convert()  # speeds up blitting
        self.image.fill((0, 0, 0, 0))
        total_pipe_body_pieces = int(
            (WIN_HEIGHT -  # fill window from top to bottom
             3 * Bird.HEIGHT -  # make room for bird to fit through
             3 * PipePair.PIECE_HEIGHT) /  # 2 end pieces + 1 body piece
            PipePair.PIECE_HEIGHT  # to get number of pipe pieces
        )

        self.bottom_pieces = randint(1, total_pipe_body_pieces)

        self.top_pieces = total_pipe_body_pieces - self.bottom_pieces

        # bottom pipe
        for i in range(1, self.bottom_pieces + 1):
            piece_pos = (0, WIN_HEIGHT - i * PipePair.PIECE_HEIGHT)
            self.image.blit(pipe_body_img, piece_pos)
        bottom_pipe_end_y = WIN_HEIGHT - self.bottom_height_px
        self.bottom_pipe_end_y = bottom_pipe_end_y

        bottom_end_piece_pos = (0, bottom_pipe_end_y - PipePair.PIECE_HEIGHT)

        self.image.blit(pipe_end_img, bottom_end_piece_pos)

        # top pipe
        for i in range(self.top_pieces):
            self.image.blit(pipe_body_img, (0, i * PipePair.PIECE_HEIGHT))
        top_pipe_end_y = self.top_height_px
        self.top_pipe_end_y = top_pipe_end_y

        self.image.blit(pipe_end_img, (0, top_pipe_end_y))

        # compensate for added end pieces
        self.top_pieces += 1
        self.bottom_pieces += 1

        # for collision detection
        self.mask = pygame.mask.from_surface(self.image)

    @property
    def top_height_px(self):
        """Get the top pipe's height, in pixels."""
        return self.top_pieces * PipePair.PIECE_HEIGHT

    @property
    def bottom_height_px(self):
        """Get the bottom pipe's height, in pixels."""
        return self.bottom_pieces * PipePair.PIECE_HEIGHT

    @property
    def visible(self):
        """Get whether this PipePair on screen, visible to the player."""
        return -PipePair.WIDTH < self.x < WIN_WIDTH

    @property
    def rect(self):
        """Get the Rect which contains this PipePair."""
        return Rect(self.x, 0, PipePair.WIDTH, PipePair.PIECE_HEIGHT)

    def update(self, delta_frames=1):
        """Update the PipePair's position.

        Arguments:
        delta_frames: The number of frames elapsed since this method was
            last called.
        """
        self.x -= ANIMATION_SPEED * frames_to_msec(delta_frames)

    def collides_with(self, bird):
        """Get whether the bird collides with a pipe in this PipePair.

        Arguments:
        bird: The Bird which should be tested for collision with this
            PipePair.
        """
        return pygame.sprite.collide_mask(self, bird)

def load_images():
    """Load all images required by the game and return a dict of them.

    The returned dict has the following keys:
    background: The game's background image.
    bird-wingup: An image of the bird with its wing pointing upward.
        Use this and bird-wingdown to create a flapping bird.
    bird-wingdown: An image of the bird with its wing pointing downward.
        Use this and bird-wingup to create a flapping bird.
    pipe-end: An image of a pipe's end piece (the slightly wider bit).
        Use this and pipe-body to make pipes.
    pipe-body: An image of a slice of a pipe's body.  Use this and
        pipe-body to make pipes.
    """

    def load_image(img_file_name):
        """Return the loaded pygame image with the specified file name.

        This function looks for images in the game's images folder
        (./images/).  All images are converted before being returned to
        speed up blitting.

        Arguments:
        img_file_name: The file name (including its extension, e.g.
            '.png') of the required image, without a file path.
        """
        file_name = os.path.join('.', 'images', img_file_name)
        img = pygame.image.load(file_name)
        img.convert()
        return img

    return {'background': load_image('background.png'),
            'pipe-end': load_image('pipe_end.png'),
            'pipe-body': load_image('pipe_body.png'),
            # images for animating the flapping bird -- animated GIFs are
            # not supported in pygame
            'bird-wingup': load_image('bird_wing_up.png'),
            'bird-wingdown': load_image('bird_wing_down.png')}

def frames_to_msec(frames, fps=FPS):
    """Convert frames to milliseconds at the specified framerate.

    Arguments:
    frames: How many frames to convert to milliseconds.
    fps: The framerate to use for conversion.  Default: FPS.
    """
    return 1000.0 * frames / fps

def msec_to_frames(milliseconds, fps=FPS):
    """Convert milliseconds to frames at the specified framerate.

    Arguments:
    milliseconds: How many milliseconds to convert to frames.
    fps: The framerate to use for conversion.  Default: FPS.
    """
    return fps * milliseconds / 1000.0

class Action(IntEnum):
    STILL = 0
    JUMP = 1

step_r = 3
step_c = 3

actions = 2
height = int((WIN_HEIGHT / step_r) * 2) + 1
width = int(((WIN_WIDTH / step_c) * 2) + PipePair.WIDTH) + 1
bird_heights = 3
pipe_heights = 3
is_jumping = 2

dimensions = (actions, height, width, bird_heights, pipe_heights, is_jumping)

Q = np.full(dimensions, 0, dtype=np.float32)
V = np.full(dimensions, 1, dtype=np.int)

def get_state(actor):
    top_y = actor.next_pipe.top_pieces * PipePair.PIECE_HEIGHT
    bottom_y = WIN_HEIGHT - actor.next_pipe.bottom_pieces * PipePair.PIECE_HEIGHT

    delta_y = int(((top_y + bottom_y) / 2) - actor.bird.y)
    delta_x = int((actor.next_pipe.x + PipePair.WIDTH) - actor.bird.x)
    bird_lmh = int(actor.bird.y / (WIN_HEIGHT / 3))
    pipe_lmh = int(((top_y + bottom_y) / 2) / (WIN_HEIGHT / 3))
    is_jumping = int(actor.bird.msec_to_climb > 0)

    bird_lmh = min(bird_lmh, 2)

    return delta_y, delta_x, bird_lmh, pipe_lmh, is_jumping

class Actor:
    def __init__(self, bird):
        self.bird = bird
        self.last_state = None
        self.action = None
        self.done = False
        self.next_pipe = None
        self.pipes_passed = 0

    def act(self, state):
        # Consult the Q matrix and pick the action that has the highest

        state = self._state_index(state)

        still_state = (Action.STILL,) + state
        jump_state = (Action.JUMP,) + state

        jump = Q[jump_state] + float(8 / V[jump_state])
        still = Q[still_state] + float(8 / V[still_state])

        if jump > still:
            action = Action.JUMP
        elif still > jump:
            action = Action.STILL
        else:
            if BIAS:
                bottom_y = WIN_HEIGHT - self.next_pipe.bottom_pieces * PipePair.PIECE_HEIGHT
                top_y = self.next_pipe.top_pieces * PipePair.PIECE_HEIGHT

                bird_lmh = int(self.bird.y / (WIN_HEIGHT / 3))
                pipe_lmh = int(((top_y + bottom_y) / 2) / (WIN_HEIGHT / 3))

                if bird_lmh > pipe_lmh:
                    action = Action.JUMP
                elif bird_lmh < pipe_lmh:
                    action = Action.STILL
                else:
                    action = Action.STILL
            else:
                if V[jump_state] < V[still_state]:
                    return Action.JUMP
                else:
                    return Action.STILL

        if INTERCEDE:
            # Intercede on the bird's behalf to stop it from dying.
            if (self.bird.y - Bird.CLIMB_DURATION * Bird.CLIMB_SPEED) <= 0:
                action = Action.STILL
            if self.bird.y + (Bird.SINK_SPEED * SKIP_FRAMES * frames_to_msec(1)) >= WIN_HEIGHT - self.bird.HEIGHT:
                action = Action.JUMP

        V[(action,) + state] += 1

        if action == Action.JUMP:
            self.bird.jump()

        return action

    def learn(self, old_state, new_state, action, reward):
        # Consult the matrix and adjust the value at the appropriate position
        old = (action,) + (self._state_index(old_state))
        new = (self._state_index(new_state))

        y, x, bird_lmh, pipe_lmh, is_jumping = new
        max_value = max(Q[:, y, x, bird_lmh, pipe_lmh, is_jumping])

        Q[old] = (1 - ALPHA) * Q[old] + ALPHA * (reward + max_value)

    @staticmethod
    def _state_index(state):
        """
        Provided a state in the form (delta_y, delta_x, bird_lmh, pipe_lmh, is_flapping) will return the adjusted forms
        of these states such that they can be used as indecies.
        """
        delta_y, delta_x, bird_lmh, pipe_lmh, is_flapping = state
        actions, height, width, _, _, _ = Q.shape

        y = int((height / 2) + (delta_y / step_r) - 1)
        x = int((width / 2) + (delta_x / step_c) - 1)

        return y, x, bird_lmh, pipe_lmh, is_flapping

def main():
    global Q, V

    pygame.init()

    display_surface = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
    pygame.display.set_caption('Pygame Flappy Bird')

    clock = pygame.time.Clock()
    score_font = pygame.font.SysFont(None, 32, bold=True)  # default font
    images = load_images()

    # the bird stays in the same x position, so bird.x is a constant
    # center bird on screen

    if not ACTOR_FILE == '':
        if os.path.exists(ACTOR_FILE):
            print('Reading actor from file.')
            with open(ACTOR_FILE, 'rb') as handle:
                Q, V = pickle.load(handle)
        else:
            print(ACTOR_FILE, 'does not exist.')

    score = 0.0
    runs = 0.0

    max_pipes_passed = 0
    pipes_passed = 0

    while True:
        runs += 1

        if runs % BLUR_FREQ == 0:
            Q = gaussian_filter(Q, sigma=SIGMA)

        print('{0:15}: AVERAGE_PIPES: {1:>15.2f} \t RUN_PIPES: {3:>5} \t OVERALL: {2:>5}'.format(int(runs), (
            (score / runs) / NUMBER_OF_BIRDS), max_pipes_passed, pipes_passed))
        birds = []

        for b in range(NUMBER_OF_BIRDS):
            x = 50

            if RANDOM_START_POINT:
                y = randint(Bird.HEIGHT, WIN_HEIGHT - 100)
            else:
                y = int(WIN_HEIGHT / 2 - Bird.HEIGHT / 2)

            birds.append(Actor(Bird(x, y, 2, (images['bird-wingup'], images['bird-wingdown']))))

        pipes = deque()
        frame_clock = 0  # this counter is only incremented if the game isn't paused

        done = paused = False
        while not done:
            clock.tick(FPS)

            # Handle this 'manually'.  If we used pygame.time.set_timer(),
            # pipe addition would be messed up when paused.
            if not (paused or frame_clock % msec_to_frames(PipePair.ADD_INTERVAL)):
                pp = PipePair(images['pipe-end'], images['pipe-body'])
                pipes.append(pp)

            for actor_bird in birds:
                actor_bird.next_pipe = pipes[0]

                if len(pipes) > 1 and (actor_bird.next_pipe.x + PipePair.WIDTH) - actor_bird.bird.x < 0:
                    actor_bird.next_pipe = pipes[1]

            for e in pygame.event.get():
                if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
                    done = True
                    break
                elif e.type == KEYUP and e.key in (K_PAUSE, K_p):
                    paused = not paused

                elif e.type == KEYUP and e.key is K_s:
                    print('{}- Writing actor to file'.format(time.strftime("%I:%M:%S")))
                    with open(SAVE_FILE, 'wb') as handle:
                        pickle.dump((Q, V), handle)

                elif e.type == MOUSEBUTTONUP or (e.type == KEYUP and e.key in (K_UP, K_RETURN, K_SPACE)):
                    for actor_bird in birds:
                        actor_bird.bird.jump()

            if paused:
                continue

            for actor_bird in birds:
                if actor_bird.last_state is None:
                    actor_bird.last_state = get_state(actor_bird)

            if frame_clock % SKIP_FRAMES == 0:
                for actor_bird in birds:
                    if not actor_bird.bird.dead:
                        actor_bird.action = actor_bird.act(actor_bird.last_state)

            # check for collisions
            all_dead = True

            for actor_bird in birds:
                bird = actor_bird.bird

                pipe_collision = any(p.collides_with(bird) for p in pipes)
                if pipe_collision or 0 >= bird.y or bird.y >= WIN_HEIGHT - Bird.HEIGHT:
                    bird.dead = True

                if not bird.dead:
                    all_dead = False

            pipes_passed = 0

            if all_dead:
                done = True

                for bird_actor in birds:
                    pipes_passed = max(pipes_passed, bird_actor.pipes_passed)
                    max_pipes_passed = max(max_pipes_passed, bird_actor.pipes_passed)

            for x in (0, WIN_WIDTH / 2):
                display_surface.blit(images['background'], (x, 0))

            while pipes and not pipes[0].visible:
                pipes.popleft()

            for p in pipes:
                p.update()
                display_surface.blit(p.image, p.rect)

            for actor_bird in birds:
                actor_bird.bird.update()
                bird = actor_bird.bird

                if frame_clock % SKIP_FRAMES == 0 or bird.dead:
                    state = get_state(actor_bird)
                    delta_y, delta_x, bird_lmh, pipe_lmh, is_jumping = state
                    distance_to_pipe = math.hypot(delta_x, delta_y)

                    reward = 0.0

                    if not actor_bird.done:
                        if not bird.dead:
                            reward += 1.0 + (15.0 / (distance_to_pipe + .1))
                        if (pipes[0].x + PipePair.WIDTH) - bird.x < 0 and id(bird) not in pipes[0].passed:
                            actor_bird.pipes_passed += 1
                            pipes[0].passed |= {id(bird)}
                            reward += 10000.0
                        if bird.dead:
                            reward -= 1000.0 + (.5 * distance_to_pipe)
                        if bird.y <= 0 or bird.y > 485:
                            reward -= 10000.0

                        actor_bird.learn(actor_bird.last_state, state, actor_bird.action, reward)

                    if bird.dead:
                        actor_bird.done = True

                    actor_bird.last_state = state

                if not bird.dead:
                    display_surface.blit(bird.image, bird.rect)

            # update and display score
            for p in pipes:
                for actor_bird in birds:
                    bird = actor_bird.bird

                    if not bird.dead:
                        if p.x + PipePair.WIDTH < bird.x and bird not in p.score_counted:
                            p.score_counted |= {bird}
                            score += 1

            for actor_bird in birds:
                pipes_passed = max(pipes_passed, actor_bird.pipes_passed)

            score_surface = score_font.render(str(pipes_passed), True, (255, 255, 255))
            score_x = WIN_WIDTH / 2 - score_surface.get_width() / 2
            display_surface.blit(score_surface, (score_x, PipePair.PIECE_HEIGHT))

            pygame.display.flip()
            frame_clock += 1
    print('Game over! Score: %i' % score)
    pygame.quit()

if __name__ == '__main__':
    main()


# -*- coding: utf-8 -*-

"""Component that manages images.
"""
import random
from dataclasses import dataclass
from functools import cached_property, partial
from typing import Optional, List

import numpy as np
from PIL import Image
from colorama import Fore

from identity_manager.image_match import ImageSignature, normalized_distance
from diamond_eye.utils import emit_and_save_event, MetaSingleton

@dataclass
class _Img:
    """Simple container for the image.
    """
    uuid: str
    filename: str
    path: str
    url: str
    path_thumb: str
    url_thumb: str
    duplicate: Optional[str]
    signature: np.array

    def __eq__(self, other) -> bool:
        """Return True if other is equal to this.
        """
        if isinstance(other, type(self)):
            return other.signature == self.signature
        return NotImplemented

    @cached_property
    def sig_tuple(self) -> tuple:
        """Return hashed signature.
        """
        return tuple(int(x) for x in self.signature)

    def __hash__(self) -> int:
        """Return hashed signature.
        """
        return hash(self.sig_tuple)

class ImageMaster(metaclass=MetaSingleton):
    """Component that manages images.
    """

    def __init__(self, images_at_once: int, threshold: float,
                 thumbnail_size: int):
        """Initialize component.
        """
        self.sig_gen = ImageSignature()
        self.images_at_once = images_at_once
        self.thumbnail_size = thumbnail_size
        self.threshold = threshold
        self._confirmation_required = []

    def get_random_image_uuids(self, state,
                               images_at_once: Optional[int] = None,
                               ) -> List[str]:
        """Get n random image urls from the state.
        """
        known_signatures = state.get_variable('app', 'known_signatures', {})
        print(f'{len(known_signatures)}, {known_signatures=}')
        images_at_once = images_at_once or self.images_at_once
        images_at_once = min(len(known_signatures), images_at_once)
        return random.sample(tuple(known_signatures.values()), images_at_once)

    def drop_all_unconfirmed(self, filesystem) -> None:
        """Delete all unconfirmed images.
        """
        for image in self._confirmation_required:
            print(Fore.RED + f'Removing: {image.filename}')
            filesystem.remove(image.path)

        self._confirmation_required.clear()

    def get_unconfirmed_images(self) -> list:
        """Get set of unconfirmed images.
        """
        return self._confirmation_required

    @staticmethod
    def compare(sig_a: tuple, sig_b: tuple) -> float:
        """Calculate distance for two signatures.
        """
        our_sig = np.array(sig_a, dtype=np.int8)
        oth_sig = np.array(sig_b, dtype=np.int8)
        return normalized_distance(our_sig, oth_sig)

    def add_to_unconfirmed(self, files: list, filesystem, state, app) -> None:
        """Add these files to list of unconfirmed temporary images.
        """
        image_data = None
        known_signatures = state.get_variable('app', 'known_signatures', {})
        print(f'p1, {len(known_signatures)}, {known_signatures=}')

        for file in files:
            path = filesystem.path('staging', 'unconfirmed', file.filename)
            url = f'/staging/unconfirmed/{file.filename}'
            file.save(path, overwrite=True)

            try:
                image_data = Image.open(path)
                new_image = _Img(
                    uuid=app.issue_and_save_new_uuid(state),
                    filename=file.filename,
                    path=path,
                    url=url,
                    path_thumb=path,
                    url_thumb=url,
                    duplicate=None,
                    signature=self.sig_gen.generate_signature(image_data),
                )
            finally:
                if image_data:
                    image_data.close()

            # FIXME - potentially slow, need full text search here
            for image in self._confirmation_required:
                distance = self.compare(image.sig_tuple, new_image.sig_tuple)

                if distance < self.threshold:
                    new_image.duplicate = image.url
                    break

            if not new_image.duplicate:
                for sig_tuple, uuid in known_signatures.items():
                    distance = self.compare(sig_tuple, new_image.sig_tuple)

                    if distance < self.threshold:
                        new_image.duplicate = state.get_variable(uuid, 'url')
                        break

            self._confirmation_required.append(new_image)

    def save_all_unconfirmed(self, filesystem, state) -> None:
        """Save all unconfirmed images.
        """
        user_id = state.get_variable('app', 'user_id')
        user_name = state.get_variable('app', 'user_name')
        version = state.get_variable('app', 'version')
        emit = partial(emit_and_save_event, state, filesystem,
                       user_id, user_name, version)

        for each in self._confirmation_required:
            if each.duplicate:
                continue

            uuid = each.uuid
            filename = each.filename
            url_thumb = self.make_thumbnail(uuid, filename, filesystem)
            url = self.confirm_image(uuid, filename, filesystem)
            sig = each.sig_tuple

            emit(branch=uuid, method='set', key='url', value=url)
            emit(branch=uuid, method='set', key='url_thumb', value=url_thumb)
            emit(branch=uuid, method='set', key='filename', value=filename)
            emit(branch=uuid, method='set', key='sig_tuple', value=sig)

            filesystem.save_metainfo(uuid, state.get_branch(uuid))
            state.set_variable('app', 'update', 'known_signatures',
                               {each.sig_tuple: uuid}, True)

        self._confirmation_required.clear()
        state.not_saved = True

    def make_thumbnail(self, uuid: str, filename: str, filesystem) -> str:
        """Make smaller version of the image and place it into thumbnails.
        """
        ext = filesystem.get_ext(filename)
        size = (self.thumbnail_size, self.thumbnail_size)

        path_from = filesystem.path('staging', 'unconfirmed', filename)
        path_thumb = filesystem.path('staging', 'thumbnails',
                                     f'{self.thumbnail_size}_{uuid}.{ext}')
        try:
            im = Image.open(path_from)
            im.thumbnail(size, Image.ANTIALIAS)
            im.save(path_thumb)
        except OSError:
            print(f'Cannot create thumbnail for {path_from}')

        url_thumb = f'/staging/thumbnails/{self.thumbnail_size}_{uuid}.{ext}'
        return url_thumb

    @staticmethod
    def confirm_image(uuid: str, filename: str, filesystem) -> str:
        """Move original image from unconfirmed to the staging.
        """
        ext = filename.rsplit('.')[-1]
        path_from = filesystem.path('staging', 'unconfirmed', filename)
        path_to = filesystem.path('staging', 'images', f'{uuid}.{ext}')
        filesystem.move(path_from, path_to)
        url = f'/staging/images/{uuid}.{ext}'
        return url


from flask import Flask, render_template, request
import numpy as np
import tensorflow as tf
import pickle
from tensorflow.keras import layers, activations, models, preprocessing
from keras.layers.wrappers import TimeDistributed
from keras.layers import Activation, Dense
import nltk
from tensorflow.keras import preprocessing, utils
import os
import yaml
from keras.preprocessing.text import Tokenizer
import re
import pickle
from gtts import gTTS
from time import sleep
import os
import pyglet

app = Flask(__name__)

maxlen_questions = 22
maxlen_answers = 74
questions = []
answers = []
with open(r"C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\questions.txt", "rb") as fp:
    questions = pickle.load(fp)
with open(r"C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\ans.txt", "rb") as fp:
    answers = pickle.load(fp)
tokenizer = preprocessing.text.Tokenizer()
tokenizer.fit_on_texts(questions + answers)
VOCAB_SIZE = len(tokenizer.word_index)+1
print('VOCAB SIZE : {}'.format(VOCAB_SIZE))

def str_to_tokens(sentence: str):
    sentence = re.sub(r'[^\w\s]', '', sentence)
    words = sentence.lower().split()
    tokens_list = list()
    for word in words:
        tokens_list.append(tokenizer.word_index[word])
    return preprocessing.sequence.pad_sequences([tokens_list], maxlen=maxlen_questions, padding='post')

def load_model(model_filename, model_weights_filename):
    with open(model_filename, 'r', encoding='utf8') as f:
        model = tf.keras.models.model_from_json(f.read())
    model.load_weights(model_weights_filename)
    return model

encoder = load_model(r'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\encoder_model\\encoder_model2.json',
                     r'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\encoder_model\\encoder_model_weights2.h5')
decoder = load_model(r'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\decoder_model\\decoder_model2.json',
                     r'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\decoder_model\\decoder_model_weights2.h5')

@app.route("/")
def index():
    tts = gTTS(
        text='WHY FEEL DEPRESSED?FEEL FREE HERE! OUR CHATBOT WILL LOOK OUT FOR YOU', lang='en')
    filename = 'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\tmp\\temp.mp3'
    tts.save(filename)
    music = pyglet.media.load(filename, streaming=False)
    music.play()
    # sleep(music.duration)
    os.remove(filename)
    return render_template("index.html")  # to send context to html

@app.route("/get")
def get_bot_response():
    userText = request.args.get("msg")
    print('Usertext: ', userText)
    userText = str(userText)
    if(userText == 'I am feeling low today'):
        tts = gTTS(text='You are not alone. All humans undergo the phase of losing self-confidence in their lives. The best way to get over such situations is by trusting your abilities and skills. Self-belief has great powers, it makes the impossible possible', lang='en')
        filename = 'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\tmp\\temp.mp3'
        tts.save(filename)
        music = pyglet.media.load(filename, streaming=False)
        music.play()
        # sleep(music.duration)
        os.remove(filename)

        return("You are not alone. All humans undergo the phase of losing self-confidence in their lives. The best way to get over such situations is by trusting your abilities and skills. Self-belief has great powers, it makes the impossible possible. "+'.')
    if(userText == 'i feel like dying'):
        tts = gTTS(text='It’s totally normal to feel overwhelmed and stressed if you’re going through a tough time, and sometimes it can feel like things will never get better.', lang='en')
        filename = 'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\tmp\\temp.mp3'
        tts.save(filename)
        music = pyglet.media.load(filename, streaming=False)
        music.play()
        # sleep(music.duration)
        os.remove(filename)
        return("It’s totally normal to feel overwhelmed and stressed if you’re going through a tough time, and sometimes it can feel like things will never get better. If you’re thinking of suicide because you can’t think of any other way out, it’s important to know that there are a number of ways you can keep yourself safe and work through your feelings.Just remember you are not alone. Just believe in yourself and things will get better. ")
    if(userText == 'i need help'):
        tts = gTTS(
            text='Dont worry, I am  here for you. Tell me what you are going through', lang='en')
        filename = 'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\tmp\\temp.mp3'
        tts.save(filename)
        music = pyglet.media.load(filename, streaming=False)
        music.play()
        sleep(music.duration)
        os.remove(filename)

        return("Don't worry, I am there here for you. Tell me what you are going through. ")
    enc_model = encoder
    dec_model = decoder
    for _ in range(10):
        states_values = enc_model.predict(str_to_tokens(userText))
        empty_target_seq = np.zeros((1, 1))
        empty_target_seq[0, 0] = tokenizer.word_index['start']
        stop_condition = False
        decoded_translation = ''

        while not stop_condition:
            dec_outputs, h, c = dec_model.predict(
                [empty_target_seq] + states_values)
            sampled_word_index = np.argmax(dec_outputs[0, -1, :])
            sampled_word = None
            for word, index in tokenizer.word_index.items():
                if sampled_word_index == index:
                    decoded_translation += ' {}'.format(word)
                    sampled_word = word
            if sampled_word == 'end' or len(decoded_translation.split()) > maxlen_answers:
                stop_condition = True
            empty_target_seq = np.zeros((1, 1))
            empty_target_seq[0, 0] = sampled_word_index
            states_values = [h, c]
        if ('https' in decoded_translation):
            decoded_translation = decoded_translation.split('https')
            s1 = decoded_translation[1].split(' ')
            s1.remove('')
            for i in range(len(s1)-2):
                s1[i] = s1[i].replace(s1[i], s1[i]+'.')
            s1.remove('end')
            s2 = 'https://'+('').join(s1)
            tts = gTTS(text=decoded_translation[0]+s2, lang='en')
            filename = 'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\tmp\\temp.mp3'
            tts.save(filename)
            music = pyglet.media.load(filename, streaming=False)
            music.play()
            # sleep(music.duration)
            os.remove(filename)
            return(decoded_translation[0]+s2+'.')
        else:
            tts = gTTS(text=decoded_translation[:-3], lang='en')
            filename = 'C:\\Users\jayit\\Downloads\\RAPID\\MedBay-V1\\Depression Bot Microservice\\tmp\\temp.mp3'
            tts.save(filename)
            music = pyglet.media.load(filename, streaming=False)
            music.play()
            # sleep(music.duration)
            os.remove(filename)
            return(decoded_translation[:-3]+'.')

if __name__ == "__main__":
    app.run(debug=True)


""" some configurations for robot peripherals
"""
import numpy as np
import collections

import pybullet as p

unitree_camera_positions = dict(
    a1= dict(
        front= [0.27, 0, 0],
        back= [-0.2, 0, 0],
        left= [0, 0.1, 0],
        right= [0, -0.1, 0],
        up= [0, 0, 0.07],
        down= [0, 0, -0.07],
    ),
    aliengo= dict(
        front= [0.35, 0, 0],
        back= [-0.33, 0, 0],
        left= [0, 0.1, 0],
        right= [0, -0.1, 0],
        up= [0, 0, 0.06],
        down= [0, 0, -0.06],
    ),
    laikago= dict(
        front= [0.3, 0, 0],
        back= [-0.3, 0, 0],
        left= [0, 0.1, 0],
        right= [0, -0.1, 0],
        up= [0, 0, 0.1],
        down= [0, 0, -0.12],
    ),
    go1= dict(
        front= [0.28, 0, 0],
        back= [-0.25, 0, 0],
        left= [-0.05, 0.09, 0],
        right= [-0.05, -0.09, 0],
        up= [0, 0, 0.06],
        down= [-0.08, 0, -0.06],
    ),
)
def get_default_camera_dict():
    return dict( # where z-axis is up direction, x-axis is right direction
        front= p.getQuaternionFromEuler([0, 0, -np.pi/2]),
        back= p.getQuaternionFromEuler([0, 0, np.pi/2]),
        left= p.getQuaternionFromEuler([0, 0, 0]),
        right= p.getQuaternionFromEuler([0, 0, np.pi]),
        up= p.getQuaternionFromEuler([np.pi/2, 0, -np.pi/2]),
        down= p.getQuaternionFromEuler([-np.pi/2, 0, -np.pi/2]),
    )
unitree_camera_orientations = collections.defaultdict(get_default_camera_dict)