File size: 5,728 Bytes
dc673ce
8993b6b
 
 
 
 
546e190
 
 
 
 
 
 
8993b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f2de5b
8993b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546e190
 
 
8993b6b
 
546e190
 
 
 
 
c6c24f6
546e190
ace6f18
 
c6c24f6
 
 
 
 
2685b9c
 
814c216
76bc9bb
8993b6b
 
546e190
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

import gradio as gr

# --- Decomposition levels mapping ---
decomposition_map = {
    "ไม่เน่า": None,
    "ท้องเขียว": (24, None),
    "Marbling": (48, None),
    "Bloating": (72, 120),
    "Facial bone exposure": (168, None),
    "Chest/abdomen organ exposure": (336, 672),
    "Nearly full skeleton": (2160, 4320),
    "Full skeleton": (4320, 8640)
}

# --- Time formatter ---
def format_time(hours):
    if hours is None:
        return None
    if hours < 24:
        return f"{hours} ชั่วโมง"
    days = hours / 24
    if days < 7:
        return f"{days:.0f} วัน"
    weeks = days / 7
    if weeks < 4:
        return f"{weeks:.0f} สัปดาห์"
    months = weeks / 4.345
    return f"{months:.0f} เดือน"

# --- Combine parameters and detect conflicts ---
def combine_params(params, enforce_cap24=False):
    lowers = [lo for _, lo, _, _ in params if lo is not None]
    uppers = [up for _, _, up, _ in params if up is not None]

    final_lower = max(lowers) if lowers else 0
    final_upper = min(uppers) if uppers else None

    if final_upper is not None and final_lower > final_upper:
        conflicting = [name for name, lo, up, _ in params
                       if (lo is not None and lo > final_upper) or
                          (up is not None and up < final_lower)]
        raise ValueError(f"!!! พารามิเตอร์ขัดแย้งกัน: {', '.join(conflicting)}")

    if enforce_cap24 and (final_upper is None or final_upper > 24):
        final_upper = 24

    details = "\n".join(detail for _, _, _, detail in params)
    return final_lower, final_upper, details

# --- TOD estimation logic ---
def estimate_tod_gui(decomposition, biceps_contraction, rigor_mortis, livor_mortis):
    params = []

    if decomposition != "ไม่เน่า":
        lo, up = decomposition_map.get(decomposition, (None, None))
        explanation = f"{decomposition}{format_time(lo)} ขึ้นไป" if lo else f"{decomposition}"
        explanation += "\nIf decomposition present, ignore other parameters"
        params.append((f"Decomposition: {decomposition}", lo, up, explanation))
        return combine_params(params, enforce_cap24=False)

    if biceps_contraction == 'Positive':
        params.append(("Biceps contraction", 0, 5, "Biceps contraction → ≤ 5 ชั่วโมง"))
    elif biceps_contraction == 'Negative':
        params.append(("Biceps contraction", 5, None, "Biceps contraction negative → ≥ 5 ชั่วโมง"))

    if rigor_mortis == 'No':
        params.append(("Rigor mortis", 0, 2, "No rigor → ≤ 2 ชั่วโมง"))
    elif rigor_mortis == 'Partial':
        params.append(("Rigor mortis", 2, 12, "Partial rigor → 2–12 ชั่วโมง"))
    elif rigor_mortis == 'Full':
        params.append(("Rigor mortis", 6, 24, "Full rigor → 6–12 ชั่วโมง แข็งต่อเนื่องอีก 12 ชั่วโมง"))
    elif rigor_mortis == 'Starting to release':
        params.append(("Rigor mortis", 18, 24, "Starting to release → 18–24 ชั่วโมง"))

    if livor_mortis == 'No':
        params.append(("Livor mortis", 0, 0.5, "No livor → ≤ 0.5 ชั่วโมง"))
    elif livor_mortis == 'Yes, Not fixed':
        params.append(("Livor mortis", 0.5, 12, "Livor not fixed → 0.5–12 ชั่วโมง"))
    elif livor_mortis == 'Yes, Not fixed, Confluence':
        params.append(("Livor mortis", 2, 12, "Livor not fixed and Confluence → 2–12 ชั่วโมง"))
    elif livor_mortis == 'Yes, Fixed':
        params.append(("Livor mortis", 8, 24, "Livor fixed → ≥ 8 ชั่วโมง"))

    return combine_params(params, enforce_cap24=True)

# --- Gradio interface function ---
def predict(decomp, biceps, rigor, livor):
    try:
        lo, up, explanation = estimate_tod_gui(decomp, biceps, rigor, livor)
        lo_str = format_time(lo)
        up_str = format_time(up) if up is not None else None

        if up_str is None:
            result_text = f"ประมาณ: ≥ {lo_str}\n\n{explanation}"
        else:
            result_text = f"ประมาณ: {lo_str}{up_str}\n\n{explanation}"
        return result_text
    except ValueError as e:
        return str(e)

# --- Gradio UI ---
with gr.Blocks(css="style.css") as demo:
    gr.Markdown("## 🕒 Time of Death Estimation Tool")
    gr.Markdown("Estimate postmortem interval based on forensic indicators.")

    with gr.Row():
        with gr.Column():
            decomp_in = gr.Dropdown(list(decomposition_map.keys()), label="🧟 Decomposition", value="ไม่เน่า")
            biceps_in = gr.Dropdown(["Positive", "Negative"], label="💪 Biceps contraction", value="Positive")
            rigor_in = gr.Dropdown(["No", "Partial", "Full", "Starting to release"], label="🧊 Rigor mortis", value="No")
            livor_in = gr.Dropdown(["No", "Yes, Not fixed", "Yes, Not fixed, Confluence", "Yes, Fixed"], label="🩸 Livor mortis", value="No")
            calculate_btn = gr.Button("🔍 Calculate")
        with gr.Column():
            output_box = gr.Textbox(label="🧾 Result", lines=10, elem_id="result_box")

    calculate_btn.click(
        predict,
        inputs=[decomp_in, biceps_in, rigor_in, livor_in],
        outputs=output_box
    )

    gr.Markdown("---")
    gr.Markdown('<a href="https://docs.google.com/forms/d/e/1FAIpQLSc2xKfhGo5fsyTiq8CcdSc_PelNbMpo42HbaehGhghnqqPz0g/viewform" target="_blank">💬 Send Feedback</a>')
    gr.Markdown("App version: 1.0.0 | Updated: August 2025 | Powered by Dr. BH")

if __name__ == "__main__":
    demo.launch()