Spaces:
Sleeping
Sleeping
Commit ·
ab65fad
1
Parent(s): 8fad80c
pushing tested revised files for HF compatibility
Browse files- .gitignore +7 -2
- app.py +56 -48
- app_local.py +150 -0
- src/preprocess.py +35 -4
- src/preprocess_local.py +28 -0
- src/processing.py +18 -10
- src/processing_local.py +97 -0
- src/storage.py +50 -27
- src/storage_local.py +59 -0
.gitignore
CHANGED
|
@@ -1,4 +1,9 @@
|
|
| 1 |
-
|
| 2 |
__pycache__/
|
| 3 |
-
*.
|
|
|
|
|
|
|
| 4 |
temp_audio_uploads/
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .gitignore
|
| 2 |
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.env
|
| 5 |
+
gcp_key.json
|
| 6 |
temp_audio_uploads/
|
| 7 |
+
temp_models/
|
| 8 |
+
*.h5
|
| 9 |
+
*.wav
|
app.py
CHANGED
|
@@ -1,62 +1,59 @@
|
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
from src.processing import train_mode_cloud, predict_health_cloud
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
#st.markdown("""<style>.stApp {background-color: #F0F2F6;}</style>""", unsafe_allow_html=True)
|
| 7 |
-
# --- PIRANAWARE COASTAL THEME (CSS) ---
|
| 8 |
st.markdown("""
|
| 9 |
<style>
|
| 10 |
-
/*
|
| 11 |
.stApp {
|
| 12 |
background-color: #000000;
|
| 13 |
}
|
| 14 |
|
| 15 |
-
/*
|
| 16 |
-
.stApp, .stMarkdown, p, label {
|
| 17 |
-
color: #FFD700 !important;
|
| 18 |
}
|
| 19 |
|
| 20 |
-
/*
|
| 21 |
-
|
| 22 |
-
color: #
|
| 23 |
-
|
| 24 |
-
font-weight: 700;
|
| 25 |
}
|
| 26 |
|
| 27 |
-
/*
|
| 28 |
-
button[data-baseweb="tab"] {
|
| 29 |
-
color: #BDB76B !important; /* Muted yellow for inactive */
|
| 30 |
-
font-weight: 600;
|
| 31 |
-
}
|
| 32 |
-
button[data-baseweb="tab"][aria-selected="true"] {
|
| 33 |
-
color: #FFD700 !important;
|
| 34 |
-
border-bottom: 4px solid #FFD700 !important;
|
| 35 |
-
background-color: #111111 !important;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
/* 5. Buttons - Black & yellow safety style */
|
| 39 |
div.stButton > button {
|
| 40 |
background-color: #000000;
|
| 41 |
color: #FFD700;
|
| 42 |
-
border:
|
| 43 |
border-radius: 8px;
|
| 44 |
font-weight: bold;
|
| 45 |
}
|
| 46 |
div.stButton > button:hover {
|
| 47 |
background-color: #FFD700;
|
| 48 |
color: #000000;
|
| 49 |
-
|
| 50 |
-
border: 3px solid #FFD700;
|
| 51 |
}
|
| 52 |
|
| 53 |
-
/*
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
color: #FFD700 !important;
|
| 56 |
-
|
| 57 |
}
|
| 58 |
|
| 59 |
-
/*
|
| 60 |
.result-box-healthy {
|
| 61 |
background-color: #111111;
|
| 62 |
border: 2px solid #00FF9C;
|
|
@@ -74,35 +71,39 @@ st.markdown("""
|
|
| 74 |
</style>
|
| 75 |
""", unsafe_allow_html=True)
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
|
| 80 |
|
| 81 |
def save_audio(audio_value):
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
audio_value.seek(0)
|
| 84 |
-
|
| 85 |
-
|
|
|
|
| 86 |
f.write(audio_value.read())
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
-
# ---
|
| 90 |
with st.sidebar:
|
| 91 |
-
#st.image("https://img.icons8.com/color/96/speedboat.png", width=80)
|
| 92 |
st.title("User Login")
|
| 93 |
st.markdown("### Ensure to use your exact boat ID")
|
| 94 |
|
| 95 |
-
# BOAT ID INPUT
|
| 96 |
boat_id = st.text_input("Enter Boat ID", value="DEMO_BOAT_01").upper().replace(" ", "_")
|
| 97 |
st.caption("Training saved online on Google Cloud.")
|
| 98 |
st.divider()
|
| 99 |
st.info(f"Active Session:\n**{boat_id}**")
|
| 100 |
|
| 101 |
-
# --- MAIN APP ---
|
| 102 |
st.title("Piranaware Boat Engine AI")
|
| 103 |
|
| 104 |
tab_train, tab_test = st.tabs(["🛠️ Train Baseline", "🩺 Diagnostics"])
|
| 105 |
|
|
|
|
| 106 |
with tab_train:
|
| 107 |
st.info(f"Training models for: **{boat_id}**. Ensure engine is HEALTHY.")
|
| 108 |
c1, c2, c3 = st.columns(3)
|
|
@@ -110,8 +111,11 @@ with tab_train:
|
|
| 110 |
for col, mode in [(c1, "idle"), (c2, "slow"), (c3, "fast")]:
|
| 111 |
with col:
|
| 112 |
st.markdown(f"### {mode.upper()}")
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
if st.button(f"Train {mode.upper()}", key=f"btn_{mode}"):
|
| 117 |
if audio:
|
|
@@ -122,6 +126,7 @@ with tab_train:
|
|
| 122 |
else:
|
| 123 |
st.error("No Audio")
|
| 124 |
|
|
|
|
| 125 |
with tab_test:
|
| 126 |
st.divider()
|
| 127 |
st.markdown(f"### Diagnostics for: **{boat_id}**")
|
|
@@ -129,8 +134,11 @@ with tab_test:
|
|
| 129 |
col_in, col_out = st.columns([1, 2])
|
| 130 |
with col_in:
|
| 131 |
mode = st.selectbox("Select Mode", ["idle", "slow", "fast"])
|
| 132 |
-
try:
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 134 |
btn = st.button("Run Diagnostics")
|
| 135 |
|
| 136 |
with col_out:
|
|
@@ -140,8 +148,8 @@ with tab_test:
|
|
| 140 |
report = predict_health_cloud(path, mode, boat_id)
|
| 141 |
|
| 142 |
if "HEALTHY" in report:
|
| 143 |
-
st.
|
| 144 |
elif "ANOMALY" in report:
|
| 145 |
-
st.
|
| 146 |
else:
|
| 147 |
st.warning(report)
|
|
|
|
| 1 |
+
#this is the app.py file created for Hugging Face. If this causes problems, revert to app_local.py - which works fine
|
| 2 |
import streamlit as st
|
| 3 |
+
|
| 4 |
+
# --- 1. CONFIG MUST BE FIRST (CRITICAL FIX) ---
|
| 5 |
+
st.set_page_config(
|
| 6 |
+
page_title="Piranaware",
|
| 7 |
+
layout="wide",
|
| 8 |
+
page_icon="🛥️"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
import os
|
| 12 |
from src.processing import train_mode_cloud, predict_health_cloud
|
| 13 |
|
| 14 |
+
# --- 2. CSS STYLING (THEME) ---
|
|
|
|
|
|
|
| 15 |
st.markdown("""
|
| 16 |
<style>
|
| 17 |
+
/* Main Background - Pure black */
|
| 18 |
.stApp {
|
| 19 |
background-color: #000000;
|
| 20 |
}
|
| 21 |
|
| 22 |
+
/* Text Color - Safety Yellow */
|
| 23 |
+
.stApp, .stMarkdown, p, label, h1, h2, h3, h4, h5, h6, .stTextInput > label {
|
| 24 |
+
color: #FFD700 !important;
|
| 25 |
}
|
| 26 |
|
| 27 |
+
/* Input Fields (Text Input, etc) - Dark Grey Background */
|
| 28 |
+
.stTextInput > div > div > input {
|
| 29 |
+
color: #FFD700;
|
| 30 |
+
background-color: #111111;
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
+
/* Buttons - Black & Yellow */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
div.stButton > button {
|
| 35 |
background-color: #000000;
|
| 36 |
color: #FFD700;
|
| 37 |
+
border: 2px solid #FFD700;
|
| 38 |
border-radius: 8px;
|
| 39 |
font-weight: bold;
|
| 40 |
}
|
| 41 |
div.stButton > button:hover {
|
| 42 |
background-color: #FFD700;
|
| 43 |
color: #000000;
|
| 44 |
+
border: 2px solid #FFD700;
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
+
/* Tabs */
|
| 48 |
+
button[data-baseweb="tab"] {
|
| 49 |
+
color: #BDB76B !important;
|
| 50 |
+
}
|
| 51 |
+
button[data-baseweb="tab"][aria-selected="true"] {
|
| 52 |
color: #FFD700 !important;
|
| 53 |
+
border-bottom: 4px solid #FFD700 !important;
|
| 54 |
}
|
| 55 |
|
| 56 |
+
/* Result Boxes */
|
| 57 |
.result-box-healthy {
|
| 58 |
background-color: #111111;
|
| 59 |
border: 2px solid #00FF9C;
|
|
|
|
| 71 |
</style>
|
| 72 |
""", unsafe_allow_html=True)
|
| 73 |
|
| 74 |
+
# --- 3. LINUX TEMP PATH SETUP ---
|
| 75 |
+
TEMP_AUDIO_PATH = "/tmp/input.wav"
|
|
|
|
| 76 |
|
| 77 |
def save_audio(audio_value):
|
| 78 |
+
"""Saves audio to /tmp/input.wav for Linux/Docker compatibility"""
|
| 79 |
+
if audio_value is None:
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
# Reset pointer to start of file
|
| 83 |
audio_value.seek(0)
|
| 84 |
+
|
| 85 |
+
# Write to the absolute path in /tmp
|
| 86 |
+
with open(TEMP_AUDIO_PATH, "wb") as f:
|
| 87 |
f.write(audio_value.read())
|
| 88 |
+
|
| 89 |
+
return TEMP_AUDIO_PATH
|
| 90 |
|
| 91 |
+
# --- 4. SIDEBAR ---
|
| 92 |
with st.sidebar:
|
|
|
|
| 93 |
st.title("User Login")
|
| 94 |
st.markdown("### Ensure to use your exact boat ID")
|
| 95 |
|
|
|
|
| 96 |
boat_id = st.text_input("Enter Boat ID", value="DEMO_BOAT_01").upper().replace(" ", "_")
|
| 97 |
st.caption("Training saved online on Google Cloud.")
|
| 98 |
st.divider()
|
| 99 |
st.info(f"Active Session:\n**{boat_id}**")
|
| 100 |
|
| 101 |
+
# --- 5. MAIN APP ---
|
| 102 |
st.title("Piranaware Boat Engine AI")
|
| 103 |
|
| 104 |
tab_train, tab_test = st.tabs(["🛠️ Train Baseline", "🩺 Diagnostics"])
|
| 105 |
|
| 106 |
+
# --- TAB 1: TRAIN ---
|
| 107 |
with tab_train:
|
| 108 |
st.info(f"Training models for: **{boat_id}**. Ensure engine is HEALTHY.")
|
| 109 |
c1, c2, c3 = st.columns(3)
|
|
|
|
| 111 |
for col, mode in [(c1, "idle"), (c2, "slow"), (c3, "fast")]:
|
| 112 |
with col:
|
| 113 |
st.markdown(f"### {mode.upper()}")
|
| 114 |
+
# Try/Except handles older Streamlit versions that lack st.audio_input
|
| 115 |
+
try:
|
| 116 |
+
audio = st.audio_input(f"Rec {mode}", key=f"rec_{mode}")
|
| 117 |
+
except AttributeError:
|
| 118 |
+
audio = st.file_uploader(f"Up {mode}", type=['wav'], key=f"rec_{mode}")
|
| 119 |
|
| 120 |
if st.button(f"Train {mode.upper()}", key=f"btn_{mode}"):
|
| 121 |
if audio:
|
|
|
|
| 126 |
else:
|
| 127 |
st.error("No Audio")
|
| 128 |
|
| 129 |
+
# --- TAB 2: DIAGNOSTICS ---
|
| 130 |
with tab_test:
|
| 131 |
st.divider()
|
| 132 |
st.markdown(f"### Diagnostics for: **{boat_id}**")
|
|
|
|
| 134 |
col_in, col_out = st.columns([1, 2])
|
| 135 |
with col_in:
|
| 136 |
mode = st.selectbox("Select Mode", ["idle", "slow", "fast"])
|
| 137 |
+
try:
|
| 138 |
+
test_audio = st.audio_input("Record", key="test")
|
| 139 |
+
except AttributeError:
|
| 140 |
+
test_audio = st.file_uploader("Upload", type=['wav'], key="test")
|
| 141 |
+
|
| 142 |
btn = st.button("Run Diagnostics")
|
| 143 |
|
| 144 |
with col_out:
|
|
|
|
| 148 |
report = predict_health_cloud(path, mode, boat_id)
|
| 149 |
|
| 150 |
if "HEALTHY" in report:
|
| 151 |
+
st.markdown(f'<div class="result-box-healthy">{report}</div>', unsafe_allow_html=True)
|
| 152 |
elif "ANOMALY" in report:
|
| 153 |
+
st.markdown(f'<div class="result-box-anomaly">{report}</div>', unsafe_allow_html=True)
|
| 154 |
else:
|
| 155 |
st.warning(report)
|
app_local.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#this is the original app.py file, and has been left out for Hugging Face. It works well for Streamlit locallly
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import os
|
| 6 |
+
from src.processing import train_mode_cloud, predict_health_cloud
|
| 7 |
+
|
| 8 |
+
#st.set_page_config(page_title="Piranaware Cloud", page_icon="☁️", layout="wide")
|
| 9 |
+
#st.markdown("""<style>.stApp {background-color: #F0F2F6;}</style>""", unsafe_allow_html=True)
|
| 10 |
+
# --- PIRANAWARE COASTAL THEME (CSS) ---
|
| 11 |
+
st.markdown("""
|
| 12 |
+
<style>
|
| 13 |
+
/* 1. Main Background - Pure black for maximum contrast */
|
| 14 |
+
.stApp {
|
| 15 |
+
background-color: #000000;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/* 2. Text Color Fix - High visibility yellow */
|
| 19 |
+
.stApp, .stMarkdown, p, label {
|
| 20 |
+
color: #FFD700 !important; /* Bright safety yellow */
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* 3. Headers - Strong yellow, slightly warmer */
|
| 24 |
+
h1, h2, h3, h4, h5, h6 {
|
| 25 |
+
color: #FFEB3B !important; /* Vivid header yellow */
|
| 26 |
+
font-family: 'Helvetica Neue', sans-serif;
|
| 27 |
+
font-weight: 700;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* 4. Tab Styling */
|
| 31 |
+
button[data-baseweb="tab"] {
|
| 32 |
+
color: #BDB76B !important; /* Muted yellow for inactive */
|
| 33 |
+
font-weight: 600;
|
| 34 |
+
}
|
| 35 |
+
button[data-baseweb="tab"][aria-selected="true"] {
|
| 36 |
+
color: #FFD700 !important;
|
| 37 |
+
border-bottom: 4px solid #FFD700 !important;
|
| 38 |
+
background-color: #111111 !important;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* 5. Buttons - Black & yellow safety style */
|
| 42 |
+
div.stButton > button {
|
| 43 |
+
background-color: #000000;
|
| 44 |
+
color: #FFD700;
|
| 45 |
+
border: 3px solid #FFD700;
|
| 46 |
+
border-radius: 8px;
|
| 47 |
+
font-weight: bold;
|
| 48 |
+
}
|
| 49 |
+
div.stButton > button:hover {
|
| 50 |
+
background-color: #FFD700;
|
| 51 |
+
color: #000000;
|
| 52 |
+
box-shadow: 0 4px 14px rgba(255, 215, 0, 0.6);
|
| 53 |
+
border: 3px solid #FFD700;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* 6. Input Labels */
|
| 57 |
+
.stAudioInput label, .stFileUploader label, .stSelectbox label, .stTextInput label {
|
| 58 |
+
color: #FFD700 !important;
|
| 59 |
+
font-weight: 700;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* 7. Results Box Styling */
|
| 63 |
+
.result-box-healthy {
|
| 64 |
+
background-color: #111111;
|
| 65 |
+
border: 2px solid #00FF9C;
|
| 66 |
+
border-left: 6px solid #00C781;
|
| 67 |
+
padding: 15px; border-radius: 5px;
|
| 68 |
+
color: #00FF9C;
|
| 69 |
+
}
|
| 70 |
+
.result-box-anomaly {
|
| 71 |
+
background-color: #111111;
|
| 72 |
+
border: 2px solid #FF5252;
|
| 73 |
+
border-left: 6px solid #D32F2F;
|
| 74 |
+
padding: 15px; border-radius: 5px;
|
| 75 |
+
color: #FF5252;
|
| 76 |
+
}
|
| 77 |
+
</style>
|
| 78 |
+
""", unsafe_allow_html=True)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
TEMP_AUDIO_DIR = "temp_audio_uploads"
|
| 82 |
+
os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
|
| 83 |
+
|
| 84 |
+
def save_audio(audio_value):
|
| 85 |
+
if audio_value is None: return None
|
| 86 |
+
audio_value.seek(0)
|
| 87 |
+
save_path = os.path.join(TEMP_AUDIO_DIR, "input.wav")
|
| 88 |
+
with open(save_path, "wb") as f:
|
| 89 |
+
f.write(audio_value.read())
|
| 90 |
+
return save_path
|
| 91 |
+
|
| 92 |
+
# --- LOGIN ---
|
| 93 |
+
with st.sidebar:
|
| 94 |
+
#st.image("https://img.icons8.com/color/96/speedboat.png", width=80)
|
| 95 |
+
st.title("User Login")
|
| 96 |
+
st.markdown("### Ensure to use your exact boat ID")
|
| 97 |
+
|
| 98 |
+
# BOAT ID INPUT
|
| 99 |
+
boat_id = st.text_input("Enter Boat ID", value="DEMO_BOAT_01").upper().replace(" ", "_")
|
| 100 |
+
st.caption("Training saved online on Google Cloud.")
|
| 101 |
+
st.divider()
|
| 102 |
+
st.info(f"Active Session:\n**{boat_id}**")
|
| 103 |
+
|
| 104 |
+
# --- MAIN APP ---
|
| 105 |
+
st.title("Piranaware Boat Engine AI")
|
| 106 |
+
|
| 107 |
+
tab_train, tab_test = st.tabs(["🛠️ Train Baseline", "🩺 Diagnostics"])
|
| 108 |
+
|
| 109 |
+
with tab_train:
|
| 110 |
+
st.info(f"Training models for: **{boat_id}**. Ensure engine is HEALTHY.")
|
| 111 |
+
c1, c2, c3 = st.columns(3)
|
| 112 |
+
|
| 113 |
+
for col, mode in [(c1, "idle"), (c2, "slow"), (c3, "fast")]:
|
| 114 |
+
with col:
|
| 115 |
+
st.markdown(f"### {mode.upper()}")
|
| 116 |
+
try: audio = st.audio_input(f"Rec {mode}", key=f"rec_{mode}")
|
| 117 |
+
except: audio = st.file_uploader(f"Up {mode}", type=['wav'], key=f"rec_{mode}")
|
| 118 |
+
|
| 119 |
+
if st.button(f"Train {mode.upper()}", key=f"btn_{mode}"):
|
| 120 |
+
if audio:
|
| 121 |
+
path = save_audio(audio)
|
| 122 |
+
with st.spinner("Training & Uploading to Cloud..."):
|
| 123 |
+
res = train_mode_cloud(path, mode, boat_id)
|
| 124 |
+
st.success(res)
|
| 125 |
+
else:
|
| 126 |
+
st.error("No Audio")
|
| 127 |
+
|
| 128 |
+
with tab_test:
|
| 129 |
+
st.divider()
|
| 130 |
+
st.markdown(f"### Diagnostics for: **{boat_id}**")
|
| 131 |
+
|
| 132 |
+
col_in, col_out = st.columns([1, 2])
|
| 133 |
+
with col_in:
|
| 134 |
+
mode = st.selectbox("Select Mode", ["idle", "slow", "fast"])
|
| 135 |
+
try: test_audio = st.audio_input("Record", key="test")
|
| 136 |
+
except: test_audio = st.file_uploader("Upload", type=['wav'], key="test")
|
| 137 |
+
btn = st.button("Run Diagnostics")
|
| 138 |
+
|
| 139 |
+
with col_out:
|
| 140 |
+
if btn and test_audio:
|
| 141 |
+
path = save_audio(test_audio)
|
| 142 |
+
with st.spinner("Downloading Model & Analyzing..."):
|
| 143 |
+
report = predict_health_cloud(path, mode, boat_id)
|
| 144 |
+
|
| 145 |
+
if "HEALTHY" in report:
|
| 146 |
+
st.success(report)
|
| 147 |
+
elif "ANOMALY" in report:
|
| 148 |
+
st.error(report)
|
| 149 |
+
else:
|
| 150 |
+
st.warning(report)
|
src/preprocess.py
CHANGED
|
@@ -1,26 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import librosa
|
| 2 |
import numpy as np
|
|
|
|
| 3 |
|
|
|
|
| 4 |
SAMPLE_RATE = 22050
|
| 5 |
-
DURATION = 1.0
|
| 6 |
SAMPLES_PER_SLICE = int(SAMPLE_RATE * DURATION)
|
| 7 |
-
N_MELS = 128
|
| 8 |
|
| 9 |
def audio_to_spectrograms(file_path):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
y, sr = librosa.load(file_path, sr=SAMPLE_RATE)
|
|
|
|
|
|
|
| 12 |
num_slices = len(y) // SAMPLES_PER_SLICE
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
|
|
|
| 15 |
spectrograms = []
|
| 16 |
for i in range(num_slices):
|
|
|
|
| 17 |
y_slice = y[i*SAMPLES_PER_SLICE : (i+1)*SAMPLES_PER_SLICE]
|
|
|
|
|
|
|
| 18 |
spec = librosa.feature.melspectrogram(y=y_slice, sr=sr, n_mels=N_MELS)
|
|
|
|
|
|
|
| 19 |
log_spec = librosa.power_to_db(spec, ref=np.max)
|
| 20 |
norm_spec = np.clip((log_spec + 80) / 80, 0, 1)
|
|
|
|
|
|
|
| 21 |
spectrograms.append(norm_spec[..., np.newaxis])
|
| 22 |
|
| 23 |
return np.array(spectrograms)
|
|
|
|
| 24 |
except Exception as e:
|
| 25 |
-
print(f"Error: {e}")
|
| 26 |
return None
|
|
|
|
| 1 |
+
#this version was created for hugging face. if issues persist revert to preprocess_local
|
| 2 |
+
|
| 3 |
+
|
| 4 |
import librosa
|
| 5 |
import numpy as np
|
| 6 |
+
import os
|
| 7 |
|
| 8 |
+
# --- CONSTANTS ---
|
| 9 |
SAMPLE_RATE = 22050
|
| 10 |
+
DURATION = 1.0 # Slice length (Seconds)
|
| 11 |
SAMPLES_PER_SLICE = int(SAMPLE_RATE * DURATION)
|
| 12 |
+
N_MELS = 128 # Frequency resolution
|
| 13 |
|
| 14 |
def audio_to_spectrograms(file_path):
|
| 15 |
+
"""
|
| 16 |
+
Converts audio into a batch of 1-second spectrogram slices.
|
| 17 |
+
Returns shape: (Num_Slices, 128, 44, 1)
|
| 18 |
+
"""
|
| 19 |
try:
|
| 20 |
+
# 1. Safety Check: File Existence
|
| 21 |
+
if not os.path.exists(file_path):
|
| 22 |
+
print(f"❌ Error: File not found at {file_path}")
|
| 23 |
+
return None
|
| 24 |
+
|
| 25 |
+
# 2. Load Audio
|
| 26 |
+
# We enforce sr=22050 for consistency
|
| 27 |
y, sr = librosa.load(file_path, sr=SAMPLE_RATE)
|
| 28 |
+
|
| 29 |
+
# 3. Calculate Slices
|
| 30 |
num_slices = len(y) // SAMPLES_PER_SLICE
|
| 31 |
+
|
| 32 |
+
# Safety: If audio is too short (< 1 second), fail gracefully
|
| 33 |
+
if num_slices < 1:
|
| 34 |
+
print("❌ Audio too short")
|
| 35 |
+
return None
|
| 36 |
|
| 37 |
+
# 4. Create Spectrograms
|
| 38 |
spectrograms = []
|
| 39 |
for i in range(num_slices):
|
| 40 |
+
# Extract 1-second chunk
|
| 41 |
y_slice = y[i*SAMPLES_PER_SLICE : (i+1)*SAMPLES_PER_SLICE]
|
| 42 |
+
|
| 43 |
+
# Generate Mel Spectrogram
|
| 44 |
spec = librosa.feature.melspectrogram(y=y_slice, sr=sr, n_mels=N_MELS)
|
| 45 |
+
|
| 46 |
+
# Convert to Decibels (Log Scale) and Normalize (0-1)
|
| 47 |
log_spec = librosa.power_to_db(spec, ref=np.max)
|
| 48 |
norm_spec = np.clip((log_spec + 80) / 80, 0, 1)
|
| 49 |
+
|
| 50 |
+
# Add Channel Dimension (Required for CNNs)
|
| 51 |
spectrograms.append(norm_spec[..., np.newaxis])
|
| 52 |
|
| 53 |
return np.array(spectrograms)
|
| 54 |
+
|
| 55 |
except Exception as e:
|
| 56 |
+
print(f"❌ Preprocessing Error: {e}")
|
| 57 |
return None
|
src/preprocess_local.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#this is the actual preprocess file. it is being replaced with a Huggging Face friendly one. if needed revert to this.
|
| 2 |
+
|
| 3 |
+
import librosa
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
SAMPLE_RATE = 22050
|
| 7 |
+
DURATION = 1.0
|
| 8 |
+
SAMPLES_PER_SLICE = int(SAMPLE_RATE * DURATION)
|
| 9 |
+
N_MELS = 128
|
| 10 |
+
|
| 11 |
+
def audio_to_spectrograms(file_path):
|
| 12 |
+
try:
|
| 13 |
+
y, sr = librosa.load(file_path, sr=SAMPLE_RATE)
|
| 14 |
+
num_slices = len(y) // SAMPLES_PER_SLICE
|
| 15 |
+
if num_slices < 1: return None
|
| 16 |
+
|
| 17 |
+
spectrograms = []
|
| 18 |
+
for i in range(num_slices):
|
| 19 |
+
y_slice = y[i*SAMPLES_PER_SLICE : (i+1)*SAMPLES_PER_SLICE]
|
| 20 |
+
spec = librosa.feature.melspectrogram(y=y_slice, sr=sr, n_mels=N_MELS)
|
| 21 |
+
log_spec = librosa.power_to_db(spec, ref=np.max)
|
| 22 |
+
norm_spec = np.clip((log_spec + 80) / 80, 0, 1)
|
| 23 |
+
spectrograms.append(norm_spec[..., np.newaxis])
|
| 24 |
+
|
| 25 |
+
return np.array(spectrograms)
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Error: {e}")
|
| 28 |
+
return None
|
src/processing.py
CHANGED
|
@@ -1,17 +1,23 @@
|
|
| 1 |
import numpy as np
|
| 2 |
import os
|
|
|
|
| 3 |
import json
|
| 4 |
import tensorflow as tf
|
| 5 |
from src.preprocess import audio_to_spectrograms
|
| 6 |
from src.model import build_autoencoder
|
| 7 |
from src.storage import upload_file, download_file
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
if not os.path.exists(TEMP_DIR):
|
| 11 |
os.makedirs(TEMP_DIR)
|
| 12 |
|
| 13 |
def train_mode_cloud(audio_path, mode_name, boat_id):
|
| 14 |
# 1. Preprocess
|
|
|
|
| 15 |
X_train = audio_to_spectrograms(audio_path)
|
| 16 |
if X_train is None: return "❌ Audio too short (min 1 sec)."
|
| 17 |
|
|
@@ -19,24 +25,27 @@ def train_mode_cloud(audio_path, mode_name, boat_id):
|
|
| 19 |
autoencoder = build_autoencoder(X_train.shape[1:])
|
| 20 |
autoencoder.fit(X_train, X_train, epochs=40, batch_size=4, verbose=0)
|
| 21 |
|
| 22 |
-
# 3. Calculate Threshold (
|
| 23 |
reconstructions = autoencoder.predict(X_train)
|
| 24 |
mse = np.mean(np.power(X_train - reconstructions, 2), axis=(1, 2, 3))
|
| 25 |
|
| 26 |
-
|
| 27 |
threshold = float(np.mean(mse) + (2 * np.std(mse)))
|
| 28 |
|
| 29 |
-
# 4. Save Locally
|
| 30 |
model_filename = f"{mode_name}_model.h5"
|
| 31 |
meta_filename = f"{mode_name}_meta.json"
|
|
|
|
|
|
|
| 32 |
local_model_path = os.path.join(TEMP_DIR, model_filename)
|
| 33 |
local_meta_path = os.path.join(TEMP_DIR, meta_filename)
|
| 34 |
|
|
|
|
| 35 |
autoencoder.save(local_model_path, save_format='h5', include_optimizer=False)
|
| 36 |
with open(local_meta_path, 'w') as f:
|
| 37 |
json.dump({"threshold": threshold}, f)
|
| 38 |
|
| 39 |
-
# 5. Upload
|
| 40 |
u1 = upload_file(local_model_path, boat_id, model_filename)
|
| 41 |
u2 = upload_file(local_meta_path, boat_id, meta_filename)
|
| 42 |
|
|
@@ -48,10 +57,11 @@ def train_mode_cloud(audio_path, mode_name, boat_id):
|
|
| 48 |
def predict_health_cloud(audio_path, mode_name, boat_id):
|
| 49 |
model_filename = f"{mode_name}_model.h5"
|
| 50 |
meta_filename = f"{mode_name}_meta.json"
|
|
|
|
| 51 |
local_model_path = os.path.join(TEMP_DIR, model_filename)
|
| 52 |
local_meta_path = os.path.join(TEMP_DIR, meta_filename)
|
| 53 |
|
| 54 |
-
# 1. Download
|
| 55 |
d1 = download_file(boat_id, model_filename, local_model_path)
|
| 56 |
d2 = download_file(boat_id, meta_filename, local_meta_path)
|
| 57 |
|
|
@@ -69,22 +79,20 @@ def predict_health_cloud(audio_path, mode_name, boat_id):
|
|
| 69 |
if X_test is None: return "Error: Audio too short."
|
| 70 |
|
| 71 |
reconstructions = model.predict(X_test)
|
| 72 |
-
# Calculate error for each second of audio
|
| 73 |
mse = np.mean(np.power(X_test - reconstructions, 2), axis=(1, 2, 3))
|
| 74 |
|
| 75 |
# 4. Analysis
|
| 76 |
anomalies = np.sum(mse > threshold)
|
| 77 |
health_score = 100 * (1 - (anomalies / len(mse)))
|
| 78 |
|
| 79 |
-
# 5.
|
| 80 |
avg_error = np.mean(mse)
|
| 81 |
max_error = np.max(mse)
|
| 82 |
|
| 83 |
status = "🟢 HEALTHY" if health_score > 85 else "🔴 ANOMALY DETECTED"
|
| 84 |
|
| 85 |
-
# Return detailed report
|
| 86 |
return f"""
|
| 87 |
-
|
| 88 |
Confidence Score: {health_score:.1f}%
|
| 89 |
|
| 90 |
--- TECHNICAL TELEMETRY ---
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import os
|
| 3 |
+
import shutil
|
| 4 |
import json
|
| 5 |
import tensorflow as tf
|
| 6 |
from src.preprocess import audio_to_spectrograms
|
| 7 |
from src.model import build_autoencoder
|
| 8 |
from src.storage import upload_file, download_file
|
| 9 |
|
| 10 |
+
# --- CRITICAL FIX FOR CLOUD DEPLOYMENT ---
|
| 11 |
+
# We use /tmp because it is the ONLY folder guaranteed to be writable in Docker.
|
| 12 |
+
TEMP_DIR = "/tmp/temp_models"
|
| 13 |
+
|
| 14 |
+
# Create the directory if it doesn't exist
|
| 15 |
if not os.path.exists(TEMP_DIR):
|
| 16 |
os.makedirs(TEMP_DIR)
|
| 17 |
|
| 18 |
def train_mode_cloud(audio_path, mode_name, boat_id):
|
| 19 |
# 1. Preprocess
|
| 20 |
+
# audio_path is now likely "/tmp/input.wav" passed from app.py
|
| 21 |
X_train = audio_to_spectrograms(audio_path)
|
| 22 |
if X_train is None: return "❌ Audio too short (min 1 sec)."
|
| 23 |
|
|
|
|
| 25 |
autoencoder = build_autoencoder(X_train.shape[1:])
|
| 26 |
autoencoder.fit(X_train, X_train, epochs=40, batch_size=4, verbose=0)
|
| 27 |
|
| 28 |
+
# 3. Calculate Threshold (Dynamic Safety Margin)
|
| 29 |
reconstructions = autoencoder.predict(X_train)
|
| 30 |
mse = np.mean(np.power(X_train - reconstructions, 2), axis=(1, 2, 3))
|
| 31 |
|
| 32 |
+
# Using 2 Standard Deviations (Industry Standard for Strict Detection)
|
| 33 |
threshold = float(np.mean(mse) + (2 * np.std(mse)))
|
| 34 |
|
| 35 |
+
# 4. Save Locally (TO /tmp)
|
| 36 |
model_filename = f"{mode_name}_model.h5"
|
| 37 |
meta_filename = f"{mode_name}_meta.json"
|
| 38 |
+
|
| 39 |
+
# These paths now point to /tmp/temp_models/...
|
| 40 |
local_model_path = os.path.join(TEMP_DIR, model_filename)
|
| 41 |
local_meta_path = os.path.join(TEMP_DIR, meta_filename)
|
| 42 |
|
| 43 |
+
# Save to the temporary linux folder
|
| 44 |
autoencoder.save(local_model_path, save_format='h5', include_optimizer=False)
|
| 45 |
with open(local_meta_path, 'w') as f:
|
| 46 |
json.dump({"threshold": threshold}, f)
|
| 47 |
|
| 48 |
+
# 5. Upload to Google Cloud
|
| 49 |
u1 = upload_file(local_model_path, boat_id, model_filename)
|
| 50 |
u2 = upload_file(local_meta_path, boat_id, meta_filename)
|
| 51 |
|
|
|
|
| 57 |
def predict_health_cloud(audio_path, mode_name, boat_id):
|
| 58 |
model_filename = f"{mode_name}_model.h5"
|
| 59 |
meta_filename = f"{mode_name}_meta.json"
|
| 60 |
+
|
| 61 |
local_model_path = os.path.join(TEMP_DIR, model_filename)
|
| 62 |
local_meta_path = os.path.join(TEMP_DIR, meta_filename)
|
| 63 |
|
| 64 |
+
# 1. Download from Cloud to /tmp
|
| 65 |
d1 = download_file(boat_id, model_filename, local_model_path)
|
| 66 |
d2 = download_file(boat_id, meta_filename, local_meta_path)
|
| 67 |
|
|
|
|
| 79 |
if X_test is None: return "Error: Audio too short."
|
| 80 |
|
| 81 |
reconstructions = model.predict(X_test)
|
|
|
|
| 82 |
mse = np.mean(np.power(X_test - reconstructions, 2), axis=(1, 2, 3))
|
| 83 |
|
| 84 |
# 4. Analysis
|
| 85 |
anomalies = np.sum(mse > threshold)
|
| 86 |
health_score = 100 * (1 - (anomalies / len(mse)))
|
| 87 |
|
| 88 |
+
# 5. Telemetry
|
| 89 |
avg_error = np.mean(mse)
|
| 90 |
max_error = np.max(mse)
|
| 91 |
|
| 92 |
status = "🟢 HEALTHY" if health_score > 85 else "🔴 ANOMALY DETECTED"
|
| 93 |
|
|
|
|
| 94 |
return f"""
|
| 95 |
+
{status}
|
| 96 |
Confidence Score: {health_score:.1f}%
|
| 97 |
|
| 98 |
--- TECHNICAL TELEMETRY ---
|
src/processing_local.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#this is the original processing.py file, but is being revised for Hugging Face. this works well locally with Streamlit and consider using if the other has issues.
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
import tensorflow as tf
|
| 7 |
+
from src.preprocess import audio_to_spectrograms
|
| 8 |
+
from src.model import build_autoencoder
|
| 9 |
+
from src.storage import upload_file, download_file
|
| 10 |
+
|
| 11 |
+
TEMP_DIR = "temp_models"
|
| 12 |
+
if not os.path.exists(TEMP_DIR):
|
| 13 |
+
os.makedirs(TEMP_DIR)
|
| 14 |
+
|
| 15 |
+
def train_mode_cloud(audio_path, mode_name, boat_id):
|
| 16 |
+
# 1. Preprocess
|
| 17 |
+
X_train = audio_to_spectrograms(audio_path)
|
| 18 |
+
if X_train is None: return "❌ Audio too short (min 1 sec)."
|
| 19 |
+
|
| 20 |
+
# 2. Train
|
| 21 |
+
autoencoder = build_autoencoder(X_train.shape[1:])
|
| 22 |
+
autoencoder.fit(X_train, X_train, epochs=40, batch_size=4, verbose=0)
|
| 23 |
+
|
| 24 |
+
# 3. Calculate Threshold (THE FIX)
|
| 25 |
+
reconstructions = autoencoder.predict(X_train)
|
| 26 |
+
mse = np.mean(np.power(X_train - reconstructions, 2), axis=(1, 2, 3))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
threshold = float(np.mean(mse) + (2 * np.std(mse)))
|
| 30 |
+
|
| 31 |
+
# 4. Save Locally
|
| 32 |
+
model_filename = f"{mode_name}_model.h5"
|
| 33 |
+
meta_filename = f"{mode_name}_meta.json"
|
| 34 |
+
local_model_path = os.path.join(TEMP_DIR, model_filename)
|
| 35 |
+
local_meta_path = os.path.join(TEMP_DIR, meta_filename)
|
| 36 |
+
|
| 37 |
+
autoencoder.save(local_model_path, save_format='h5', include_optimizer=False)
|
| 38 |
+
with open(local_meta_path, 'w') as f:
|
| 39 |
+
json.dump({"threshold": threshold}, f)
|
| 40 |
+
|
| 41 |
+
# 5. Upload
|
| 42 |
+
u1 = upload_file(local_model_path, boat_id, model_filename)
|
| 43 |
+
u2 = upload_file(local_meta_path, boat_id, meta_filename)
|
| 44 |
+
|
| 45 |
+
if u1 and u2:
|
| 46 |
+
return f"✅ Calibrated {mode_name.upper()} | Threshold: {threshold:.5f}"
|
| 47 |
+
else:
|
| 48 |
+
return "⚠️ Trained locally, but Cloud Upload Failed."
|
| 49 |
+
|
| 50 |
+
def predict_health_cloud(audio_path, mode_name, boat_id):
|
| 51 |
+
model_filename = f"{mode_name}_model.h5"
|
| 52 |
+
meta_filename = f"{mode_name}_meta.json"
|
| 53 |
+
local_model_path = os.path.join(TEMP_DIR, model_filename)
|
| 54 |
+
local_meta_path = os.path.join(TEMP_DIR, meta_filename)
|
| 55 |
+
|
| 56 |
+
# 1. Download
|
| 57 |
+
d1 = download_file(boat_id, model_filename, local_model_path)
|
| 58 |
+
d2 = download_file(boat_id, meta_filename, local_meta_path)
|
| 59 |
+
|
| 60 |
+
if not (d1 and d2):
|
| 61 |
+
return f"⚠️ No trained model found in cloud for Boat: {boat_id} (Mode: {mode_name})"
|
| 62 |
+
|
| 63 |
+
# 2. Load
|
| 64 |
+
with open(local_meta_path, 'r') as f:
|
| 65 |
+
threshold = json.load(f)["threshold"]
|
| 66 |
+
|
| 67 |
+
model = tf.keras.models.load_model(local_model_path, compile=False)
|
| 68 |
+
|
| 69 |
+
# 3. Predict
|
| 70 |
+
X_test = audio_to_spectrograms(audio_path)
|
| 71 |
+
if X_test is None: return "Error: Audio too short."
|
| 72 |
+
|
| 73 |
+
reconstructions = model.predict(X_test)
|
| 74 |
+
# Calculate error for each second of audio
|
| 75 |
+
mse = np.mean(np.power(X_test - reconstructions, 2), axis=(1, 2, 3))
|
| 76 |
+
|
| 77 |
+
# 4. Analysis
|
| 78 |
+
anomalies = np.sum(mse > threshold)
|
| 79 |
+
health_score = 100 * (1 - (anomalies / len(mse)))
|
| 80 |
+
|
| 81 |
+
# 5. Debug Data (Shows you WHY it decided what it decided)
|
| 82 |
+
avg_error = np.mean(mse)
|
| 83 |
+
max_error = np.max(mse)
|
| 84 |
+
|
| 85 |
+
status = "🟢 HEALTHY" if health_score > 85 else "🔴 ANOMALY DETECTED"
|
| 86 |
+
|
| 87 |
+
# Return detailed report
|
| 88 |
+
return f"""
|
| 89 |
+
STATUS: {status}
|
| 90 |
+
Confidence Score: {health_score:.1f}%
|
| 91 |
+
|
| 92 |
+
--- TECHNICAL TELEMETRY ---
|
| 93 |
+
Threshold Limit : {threshold:.5f}
|
| 94 |
+
Your Avg Error : {avg_error:.5f}
|
| 95 |
+
Your Max Error : {max_error:.5f}
|
| 96 |
+
Anomalous Secs : {anomalies} / {len(mse)}
|
| 97 |
+
"""
|
src/storage.py
CHANGED
|
@@ -1,58 +1,81 @@
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import streamlit as st
|
| 4 |
from google.cloud import storage
|
| 5 |
from google.oauth2 import service_account
|
| 6 |
|
| 7 |
-
# ✅
|
| 8 |
BUCKET_NAME = "piranaware20251227841ph"
|
| 9 |
|
| 10 |
def get_storage_client():
|
| 11 |
"""
|
| 12 |
Authenticates with Google Cloud.
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
-
# 1. Local Dev: Check for local file
|
| 17 |
if os.path.exists("gcp_key.json"):
|
| 18 |
return storage.Client.from_service_account_json("gcp_key.json")
|
| 19 |
|
| 20 |
-
# 2.
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
creds_dict = dict(st.secrets["gcp_service_account"])
|
| 24 |
creds = service_account.Credentials.from_service_account_info(creds_dict)
|
| 25 |
return storage.Client(credentials=creds)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
pass
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
st.error("⚠️ No Google Cloud credentials found. Cannot save models.")
|
| 32 |
return None
|
| 33 |
|
| 34 |
def upload_file(local_path, boat_id, filename):
|
| 35 |
client = get_storage_client()
|
| 36 |
if not client: return False
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
def download_file(boat_id, filename, local_dest):
|
| 47 |
client = get_storage_client()
|
| 48 |
if not client: return False
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
if not blob.exists():
|
| 55 |
-
return False
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#this was not the original storage py file, and was created for Huggging Face. If this fails consider using storage_local.py
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import json
|
| 5 |
import streamlit as st
|
| 6 |
from google.cloud import storage
|
| 7 |
from google.oauth2 import service_account
|
| 8 |
|
| 9 |
+
# ✅ YOUR BUCKET NAME
|
| 10 |
BUCKET_NAME = "piranaware20251227841ph"
|
| 11 |
|
| 12 |
def get_storage_client():
|
| 13 |
"""
|
| 14 |
Authenticates with Google Cloud.
|
| 15 |
+
Priority:
|
| 16 |
+
1. Local File (Codespaces)
|
| 17 |
+
2. Environment Variable (Hugging Face Docker)
|
| 18 |
+
3. Streamlit Secrets (Streamlit Cloud fallback)
|
| 19 |
"""
|
| 20 |
+
# 1. Local Dev: Check for local file
|
| 21 |
if os.path.exists("gcp_key.json"):
|
| 22 |
return storage.Client.from_service_account_json("gcp_key.json")
|
| 23 |
|
| 24 |
+
# 2. Hugging Face / Docker: Check Environment Variable
|
| 25 |
+
# In HF, secrets are stored as Env Vars, so we parse the JSON string.
|
| 26 |
+
env_key = os.environ.get("gcp_service_account")
|
| 27 |
+
if env_key:
|
| 28 |
+
try:
|
| 29 |
+
creds_dict = json.loads(env_key)
|
| 30 |
+
creds = service_account.Credentials.from_service_account_info(creds_dict)
|
| 31 |
+
return storage.Client(credentials=creds)
|
| 32 |
+
except Exception as e:
|
| 33 |
+
st.error(f"⚠️ Credential Error: {e}")
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
# 3. Streamlit Cloud: Check Secrets (Fallback)
|
| 37 |
+
if "gcp_service_account" in st.secrets:
|
| 38 |
+
try:
|
| 39 |
creds_dict = dict(st.secrets["gcp_service_account"])
|
| 40 |
creds = service_account.Credentials.from_service_account_info(creds_dict)
|
| 41 |
return storage.Client(credentials=creds)
|
| 42 |
+
except Exception:
|
| 43 |
+
pass
|
|
|
|
| 44 |
|
| 45 |
+
# 4. If nothing works
|
| 46 |
+
st.error("⚠️ No Google Cloud credentials found. Cannot save/load models.")
|
| 47 |
return None
|
| 48 |
|
| 49 |
def upload_file(local_path, boat_id, filename):
|
| 50 |
client = get_storage_client()
|
| 51 |
if not client: return False
|
| 52 |
|
| 53 |
+
try:
|
| 54 |
+
bucket = client.bucket(BUCKET_NAME)
|
| 55 |
+
# Creates folder structure: boat_id/filename
|
| 56 |
+
blob_name = f"{boat_id}/{filename}"
|
| 57 |
+
blob = bucket.blob(blob_name)
|
| 58 |
+
|
| 59 |
+
blob.upload_from_filename(local_path)
|
| 60 |
+
return True
|
| 61 |
+
except Exception as e:
|
| 62 |
+
st.error(f"Upload failed: {e}")
|
| 63 |
+
return False
|
| 64 |
|
| 65 |
def download_file(boat_id, filename, local_dest):
|
| 66 |
client = get_storage_client()
|
| 67 |
if not client: return False
|
| 68 |
|
| 69 |
+
try:
|
| 70 |
+
bucket = client.bucket(BUCKET_NAME)
|
| 71 |
+
blob_name = f"{boat_id}/{filename}"
|
| 72 |
+
blob = bucket.blob(blob_name)
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
if not blob.exists():
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
blob.download_to_filename(local_dest)
|
| 78 |
+
return True
|
| 79 |
+
except Exception as e:
|
| 80 |
+
st.error(f"Download failed: {e}")
|
| 81 |
+
return False
|
src/storage_local.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#this was the original storage py file that worked well locally. revert to this if needed. the new file renamed storage py was revised for Hugging Face
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import streamlit as st
|
| 5 |
+
from google.cloud import storage
|
| 6 |
+
from google.oauth2 import service_account
|
| 7 |
+
|
| 8 |
+
# ✅ UPDATED WITH YOUR BUCKET NAME
|
| 9 |
+
BUCKET_NAME = "piranaware20251227841ph"
|
| 10 |
+
|
| 11 |
+
def get_storage_client():
|
| 12 |
+
"""
|
| 13 |
+
Authenticates with Google Cloud.
|
| 14 |
+
CHECKS LOCAL FILE FIRST (to prevent crashes in Codespaces),
|
| 15 |
+
then checks Secrets (for Hugging Face).
|
| 16 |
+
"""
|
| 17 |
+
# 1. Local Dev: Check for local file FIRST
|
| 18 |
+
if os.path.exists("gcp_key.json"):
|
| 19 |
+
return storage.Client.from_service_account_json("gcp_key.json")
|
| 20 |
+
|
| 21 |
+
# 2. Production: Check Streamlit Secrets
|
| 22 |
+
try:
|
| 23 |
+
if "gcp_service_account" in st.secrets:
|
| 24 |
+
creds_dict = dict(st.secrets["gcp_service_account"])
|
| 25 |
+
creds = service_account.Credentials.from_service_account_info(creds_dict)
|
| 26 |
+
return storage.Client(credentials=creds)
|
| 27 |
+
except Exception:
|
| 28 |
+
# If secrets don't exist, we just move on
|
| 29 |
+
pass
|
| 30 |
+
|
| 31 |
+
# 3. If neither works
|
| 32 |
+
st.error("⚠️ No Google Cloud credentials found. Cannot save models.")
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
def upload_file(local_path, boat_id, filename):
|
| 36 |
+
client = get_storage_client()
|
| 37 |
+
if not client: return False
|
| 38 |
+
|
| 39 |
+
bucket = client.bucket(BUCKET_NAME)
|
| 40 |
+
# Creates folder structure: boat_id/filename
|
| 41 |
+
blob_name = f"{boat_id}/{filename}"
|
| 42 |
+
blob = bucket.blob(blob_name)
|
| 43 |
+
|
| 44 |
+
blob.upload_from_filename(local_path)
|
| 45 |
+
return True
|
| 46 |
+
|
| 47 |
+
def download_file(boat_id, filename, local_dest):
|
| 48 |
+
client = get_storage_client()
|
| 49 |
+
if not client: return False
|
| 50 |
+
|
| 51 |
+
bucket = client.bucket(BUCKET_NAME)
|
| 52 |
+
blob_name = f"{boat_id}/{filename}"
|
| 53 |
+
blob = bucket.blob(blob_name)
|
| 54 |
+
|
| 55 |
+
if not blob.exists():
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
blob.download_to_filename(local_dest)
|
| 59 |
+
return True
|