refactor: clean up dependencies and modernize build assets for improved stability

This commit is contained in:
ws50529
2026-05-14 13:59:45 +08:00
parent a9489dafe4
commit a3a88c9830
9 changed files with 1476 additions and 1206 deletions
+38 -15
View File
@@ -1,26 +1,49 @@
import numpy as np
from fastapi import HTTPException
from scipy import signal
from .config import BODE_MAX_MULTIPLIER, BODE_POINTS
from .schemas import BodeParams
from .schemas import BodeCompareParams, BodeParams
from .validation import require_finite, validate_coefficients
def calculate_bode_response(params: BodeParams):
fs_val = require_finite(params.fs, "fs")
b_vals = validate_coefficients(params.b, "b")
a_vals = validate_coefficients(params.a, "a")
f_min = params.fs / 50000.0
f_max = params.fs * BODE_MAX_MULTIPLIER
f_eval = np.logspace(np.log10(f_min), np.log10(f_max), BODE_POINTS)
def _bode_series(b_values, a_values, f_eval, fs_val):
b_vals = validate_coefficients(b_values, "b")
a_vals = validate_coefficients(a_values, "a")
_, h = signal.freqz(b_vals, a_vals, worN=f_eval, fs=fs_val)
mag = 20 * np.log10(np.abs(h) + 1e-12)
phase = np.angle(h, deg=True)
return {
"freq": f_eval.tolist(),
"mag": mag.tolist(),
"phase": phase.tolist(),
"mag": (20 * np.log10(np.abs(h) + 1e-12)).tolist(),
"phase": np.angle(h, deg=True).tolist(),
}
def _frequency_axis(fs):
fs_val = require_finite(fs, "fs")
f_min = fs_val / 50000.0
f_max = fs_val * BODE_MAX_MULTIPLIER
return fs_val, np.logspace(np.log10(f_min), np.log10(f_max), BODE_POINTS)
def calculate_bode_response(params: BodeParams):
fs_val, f_eval = _frequency_axis(params.fs)
series = _bode_series(params.b, params.a, f_eval, fs_val)
return {
"freq": f_eval.tolist(),
"mag": series["mag"],
"phase": series["phase"],
}
def calculate_bode_compare_response(params: BodeCompareParams):
fs_val, f_eval = _frequency_axis(params.ideal.fs)
fixed_fs_val = require_finite(params.fixed.fs, "fixed.fs")
if not np.isclose(fixed_fs_val, fs_val):
raise HTTPException(status_code=400, detail="ideal 與 fixed 的 fs 必須相同")
ideal = _bode_series(params.ideal.b, params.ideal.a, f_eval, fs_val)
fixed = _bode_series(params.fixed.b, params.fixed.a, f_eval, fs_val)
return {
"freq": f_eval.tolist(),
"ideal": ideal,
"fixed": fixed,
}
+4
View File
@@ -38,3 +38,7 @@ class BodeParams(BaseModel):
a: List[float] = Field(min_length=1, max_length=MAX_COEFFICIENTS)
fs: float = Field(gt=0)
class BodeCompareParams(BaseModel):
ideal: BodeParams
fixed: BodeParams
+12 -2
View File
@@ -2,7 +2,7 @@ from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import RedirectResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from dea.bode import calculate_bode_response
from dea.bode import calculate_bode_compare_response, calculate_bode_response
from dea.config import (
BODE_MAX_MULTIPLIER,
BODE_POINTS,
@@ -19,7 +19,7 @@ from dea.csv_processing import (
read_csv_upload,
)
from dea.filter_design import design_response
from dea.schemas import BodeParams, DesignParams
from dea.schemas import BodeCompareParams, BodeParams, DesignParams
from dea.security import (
SECURITY_HEADERS,
add_security_headers,
@@ -75,6 +75,16 @@ def calculate_bode(params: BodeParams):
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/bode/compare")
def calculate_bode_compare(params: BodeCompareParams):
try:
return calculate_bode_compare_response(params)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/filter")
async def filter_csv(
file: UploadFile = File(...),
+3 -3
View File
@@ -73,7 +73,7 @@
濾波器設計
</div>
<button @click="resetFilterParams"
class="text-[10px] font-bold role-bg-warning-soft role-text-warning role-hover-warning-soft px-2 py-1 rounded transition-colors uppercase">重設設計</button>
class="text-[10px] font-bold role-bg-secondary-soft role-border-secondary-soft role-text-secondary role-hover-secondary-soft border px-2 py-1 rounded transition-colors uppercase">重設設計</button>
</h2>
<select v-model="filterType" @change="onFilterTypeChange"
class="w-full bg-slate-50 dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base role-focus-primary focus:ring-1 role-focus-ring-primary outline-none transition-all mb-3 text-slate-900 dark:text-gray-100 truncate">
@@ -267,7 +267,7 @@
濾波器係數設定</span>
<div class="flex items-center gap-2">
<button v-if="isManualDirty" @click="clearManualAdjustments"
class="text-[10px] font-bold bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 px-2 py-1 rounded text-slate-500 transition-colors uppercase mr-1">重設</button>
class="text-[10px] font-bold role-bg-secondary-soft role-border-secondary-soft role-text-secondary role-hover-secondary-soft border px-2 py-1 rounded transition-colors uppercase mr-1">重設</button>
</div>
</h2>
<div class="mb-3">
@@ -475,7 +475,7 @@
</div>
<div class="flex items-center gap-2">
<button v-if="isFixedPointDirty" @click="resetFixedPointSettings"
class="text-[10px] font-bold bg-slate-200 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 px-2 py-1 rounded text-slate-500 transition-colors uppercase mr-1">重設</button>
class="text-[10px] font-bold role-bg-secondary-soft role-border-secondary-soft role-text-secondary role-hover-secondary-soft border px-2 py-1 rounded transition-colors uppercase mr-1">重設</button>
</div>
</h2>
Regular → Executable
+1106 -1118
View File
File diff suppressed because it is too large Load Diff
+287 -64
View File
@@ -4,36 +4,42 @@
@tailwind components;
@tailwind utilities;
/* Morandi green hierarchy. Feature colors are expressed through role classes only. */
/* Pixel M3E hierarchy. Feature colors are expressed through role classes only. */
:root {
--m3-primary: #657868;
--m3-primary: #566075;
--m3-on-primary: #ffffff;
--m3-primary-container: #dce7dc;
--m3-on-primary-container: #1b2a1f;
--m3-secondary: #73786f;
--m3-primary-container: #d8deef;
--m3-on-primary-container: #202637;
--m3-secondary: #5e6472;
--m3-on-secondary: #ffffff;
--m3-secondary-container: #e2e6dc;
--m3-on-secondary-container: #20251f;
--m3-tertiary: #877b68;
--m3-secondary-container: #e1e5f0;
--m3-on-secondary-container: #242832;
--m3-tertiary: #7a6689;
--m3-on-tertiary: #ffffff;
--m3-tertiary-container: #ece2d2;
--m3-on-tertiary-container: #2d261a;
--m3-error: #a25f5b;
--m3-tertiary-container: #f0dcff;
--m3-on-tertiary-container: #30203d;
--m3-error: #b3261e;
--m3-on-error: #ffffff;
--m3-error-container: #f2ddda;
--m3-warning: #806b43;
--m3-warning-container: #f0e4c7;
--m3-background: #fbfcf8;
--m3-surface: #fbfcf8;
--m3-error-container: #f9dedc;
--m3-success: #2e7d61;
--m3-success-container: #cceede;
--m3-warning: #8c6d1f;
--m3-warning-container: #f5e4b2;
--m3-accent-cyan: #64d2ff;
--m3-accent-pink: #ff9bd8;
--m3-accent-orange: #ffb879;
--m3-accent-lavender: #b9b2ff;
--m3-background: #f6f7fb;
--m3-surface: #f6f7fb;
--m3-surface-container-lowest: #ffffff;
--m3-surface-container-low: #f4f7ef;
--m3-surface-container: #edf1e8;
--m3-surface-container-high: #e4e9de;
--m3-surface-container-highest: #dbe2d5;
--m3-on-surface: #1b1d19;
--m3-on-surface-variant: #464b43;
--m3-outline: #747b70;
--m3-outline-variant: #c9d0c3;
--m3-surface-container-low: #eff1f7;
--m3-surface-container: #e7e9f1;
--m3-surface-container-high: #dde1ec;
--m3-surface-container-highest: #d1d7e6;
--m3-on-surface: #17191f;
--m3-on-surface-variant: #525663;
--m3-outline: #747987;
--m3-outline-variant: #c3c8d4;
--m3-divider: color-mix(in srgb, var(--m3-outline-variant) 78%, transparent);
--m3-soft-outline: color-mix(in srgb, var(--m3-outline-variant) 88%, transparent);
--m3-input-outline: color-mix(in srgb, var(--m3-outline) 82%, transparent);
@@ -41,41 +47,58 @@
--m3-secondary-state: color-mix(in srgb, var(--m3-secondary) 12%, transparent);
--m3-tertiary-state: color-mix(in srgb, var(--m3-tertiary) 12%, transparent);
--m3-warning-state: color-mix(in srgb, var(--m3-warning) 12%, transparent);
--m3-shadow: 0 1px 2px rgba(29, 27, 32, 0.14), 0 2px 6px rgba(29, 27, 32, 0.10);
--m3-elevated-shadow: 0 3px 8px rgba(29, 27, 32, 0.13), 0 12px 24px rgba(29, 27, 32, 0.08);
--m3-shadow: 0 1px 2px rgba(15, 18, 26, 0.10), 0 8px 18px rgba(15, 18, 26, 0.08);
--m3-elevated-shadow: 0 8px 22px rgba(15, 18, 26, 0.12), 0 18px 36px rgba(15, 18, 26, 0.10);
--background: var(--m3-background);
--surface: var(--m3-surface-container);
--surfaceHigh: var(--m3-surface-container-high);
--primary: var(--m3-primary);
--onPrimary: var(--m3-on-primary);
--textPrimary: var(--m3-on-surface);
--textSecondary: var(--m3-on-surface-variant);
--border: var(--m3-outline-variant);
--danger: var(--m3-error);
--success: var(--m3-success);
--warning: var(--m3-warning);
}
.dark {
--m3-primary: #b7c9b8;
--m3-on-primary: #243429;
--m3-primary-container: #3f5745;
--m3-on-primary-container: #dce7dc;
--m3-secondary: #c6cbbf;
--m3-on-secondary: #2e332b;
--m3-secondary-container: #454b40;
--m3-on-secondary-container: #e2e6dc;
--m3-tertiary: #d4c5aa;
--m3-on-tertiary: #3b2f20;
--m3-tertiary-container: #5d503e;
--m3-on-tertiary-container: #f1e4cf;
--m3-error: #e6b8b4;
--m3-on-error: #5f1715;
--m3-error-container: #7c3430;
--m3-warning: #dcc48e;
--m3-warning-container: #5a4828;
--m3-background: #111510;
--m3-surface: #111510;
--m3-surface-container-lowest: #0b0e0a;
--m3-surface-container-low: #1a1f18;
--m3-surface-container: #20261e;
--m3-surface-container-high: #2a3128;
--m3-surface-container-highest: #343d31;
--m3-on-surface: #e5e9df;
--m3-on-surface-variant: #c7cec1;
--m3-outline: #92998e;
--m3-outline-variant: #464d43;
--m3-shadow: 0 1px 2px rgba(0, 0, 0, 0.36), 0 4px 12px rgba(0, 0, 0, 0.28);
--m3-elevated-shadow: 0 4px 12px rgba(0, 0, 0, 0.45), 0 16px 32px rgba(0, 0, 0, 0.32);
--m3-primary: #c8cee0;
--m3-on-primary: #202431;
--m3-primary-container: #3f4453;
--m3-on-primary-container: #eef1ff;
--m3-secondary: #aeb6c9;
--m3-on-secondary: #242832;
--m3-secondary-container: #343842;
--m3-on-secondary-container: #e4e8f6;
--m3-tertiary: #d6b7f0;
--m3-on-tertiary: #382047;
--m3-tertiary-container: #4b3b59;
--m3-on-tertiary-container: #f4dcff;
--m3-error: #f2b8b5;
--m3-on-error: #601410;
--m3-error-container: #7d2e29;
--m3-success: #9bd7bd;
--m3-success-container: #254c3a;
--m3-warning: #e6cb7f;
--m3-warning-container: #55451f;
--m3-accent-cyan: #5fd4ff;
--m3-accent-pink: #ff9bd8;
--m3-accent-orange: #ffb879;
--m3-accent-lavender: #b9b2ff;
--m3-background: #111216;
--m3-surface: #111216;
--m3-surface-container-lowest: #090a0d;
--m3-surface-container-low: #1a1b20;
--m3-surface-container: #2a2a2e;
--m3-surface-container-high: #34353b;
--m3-surface-container-highest: #41434b;
--m3-on-surface: #f1f1f4;
--m3-on-surface-variant: #b4b4bd;
--m3-outline: #8b8d98;
--m3-outline-variant: #4b4d56;
--m3-shadow: 0 1px 2px rgba(0, 0, 0, 0.42), 0 8px 24px rgba(0, 0, 0, 0.34);
--m3-elevated-shadow: 0 8px 24px rgba(0, 0, 0, 0.48), 0 24px 48px rgba(0, 0, 0, 0.36);
}
html,
@@ -87,7 +110,16 @@ body,
body {
font-family: "Google Sans", Roboto, "Noto Sans TC", system-ui, -apple-system, sans-serif;
font-size: 16px;
line-height: 1.55;
letter-spacing: 0;
text-rendering: optimizeLegibility;
}
#app {
background:
radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--m3-primary) 10%, transparent), transparent 30rem),
var(--m3-background);
}
header,
@@ -106,11 +138,26 @@ header.bg-white,
header.dark\:bg-dark,
footer.bg-white,
footer.dark\:bg-dark {
background: var(--m3-surface-container-low) !important;
background: color-mix(in srgb, var(--m3-background) 82%, var(--m3-surface-container) 18%) !important;
border-color: var(--m3-divider) !important;
box-shadow: none !important;
}
header {
min-height: 72px;
padding-inline: clamp(1rem, 2vw, 1.75rem) !important;
}
header img {
background: var(--m3-surface-container-high);
border-radius: 18px !important;
}
header h1 {
font-size: clamp(1.35rem, 1.8vw, 2rem) !important;
line-height: 1.15;
}
aside.bg-white,
aside.dark\:bg-dark,
main.bg-slate-50,
@@ -133,7 +180,23 @@ header h1 {
background: none !important;
color: var(--m3-on-surface) !important;
-webkit-text-fill-color: var(--m3-on-surface);
font-weight: 700;
font-weight: 500;
letter-spacing: 0;
}
h2 {
font-size: 0.95rem !important;
font-weight: 500 !important;
letter-spacing: 0.01em !important;
text-transform: none !important;
}
h3.font-bold {
font-size: clamp(1.15rem, 1.35vw, 1.45rem) !important;
}
h4.font-bold {
font-size: 1.05rem !important;
}
hr {
@@ -168,6 +231,22 @@ main > div,
box-shadow: none !important;
}
aside section {
background: var(--m3-surface-container) !important;
border: 1px solid var(--m3-soft-outline);
border-radius: 28px;
padding: 1.15rem;
}
main > div {
border-radius: 30px !important;
overflow: hidden;
}
main > div > div:first-child {
border-radius: 30px 30px 0 0;
}
.border,
.border-2 {
border-color: var(--m3-outline-variant) !important;
@@ -222,7 +301,7 @@ input[type="number"],
textarea {
background: var(--m3-surface-container-lowest) !important;
border-color: var(--m3-input-outline) !important;
border-radius: 12px !important;
border-radius: 22px !important;
color: var(--m3-on-surface) !important;
box-shadow: none;
}
@@ -248,10 +327,17 @@ label.cursor-pointer {
border-color: transparent !important;
border-radius: 999px !important;
box-shadow: none !important;
font-weight: 650;
font-weight: 500;
transition: background-color 140ms ease, color 140ms ease, box-shadow 140ms ease, transform 120ms ease;
}
button:focus-visible,
[role="button"]:focus-visible,
label.cursor-pointer:focus-visible {
outline: 3px solid color-mix(in srgb, var(--m3-primary) 42%, transparent);
outline-offset: 2px;
}
button:hover,
[role="button"]:hover,
label.cursor-pointer:hover {
@@ -297,7 +383,7 @@ details,
select,
textarea,
input[type="number"] {
border-radius: 16px !important;
border-radius: 22px !important;
}
.rounded-full {
@@ -305,15 +391,17 @@ input[type="number"] {
}
input[type=range] {
background: color-mix(in srgb, var(--m3-primary) 22%, var(--m3-surface-container-high)) !important;
background: color-mix(in srgb, var(--m3-primary) 28%, var(--m3-surface-container-high)) !important;
border-radius: 999px;
height: 6px;
height: 10px;
}
input[type=range]::-webkit-slider-thumb {
background: var(--m3-primary);
border: 3px solid var(--m3-surface-container-lowest);
border: 4px solid var(--m3-surface-container-lowest);
box-shadow: var(--m3-shadow);
height: 28px;
width: 28px;
}
.dark input[type=range]::-webkit-slider-thumb {
@@ -529,3 +617,138 @@ details .grid.grid-cols-\[2rem_1fr_2rem\] button {
background: var(--m3-secondary-container) !important;
color: var(--m3-on-secondary-container) !important;
}
details .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > button,
details .grid.grid-cols-\[2rem_1fr_2rem\] > button,
details button.touch-none {
background: var(--m3-secondary-container) !important;
border: 1px solid color-mix(in srgb, var(--m3-secondary) 28%, var(--m3-outline-variant)) !important;
color: var(--m3-on-secondary-container) !important;
border-radius: 999px !important;
min-height: 42px;
}
details .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > button:hover,
details .grid.grid-cols-\[2rem_1fr_2rem\] > button:hover,
details button.touch-none:hover {
background: color-mix(in srgb, var(--m3-secondary-container) 82%, var(--m3-primary) 18%) !important;
color: var(--m3-on-secondary-container) !important;
}
details .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > div,
details .grid.grid-cols-\[2rem_1fr_2rem\] input,
details button.touch-none,
details .grid.grid-cols-2.gap-2 > div {
background: var(--m3-surface-container-lowest) !important;
border-color: var(--m3-soft-outline) !important;
}
details .grid.grid-cols-\[2rem_1fr_2rem\] input,
details button.touch-none span.font-mono,
details .grid.grid-cols-2.gap-2 > div span.font-mono {
color: var(--m3-on-surface) !important;
-webkit-text-fill-color: var(--m3-on-surface);
}
details .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > div span.font-mono {
color: var(--m3-on-secondary-container) !important;
-webkit-text-fill-color: var(--m3-on-secondary-container);
}
details .grid.grid-cols-\[2rem_1fr_2rem\] input:focus {
border-color: var(--m3-primary) !important;
box-shadow: 0 0 0 3px var(--m3-primary-state) !important;
}
.grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > button,
.grid.grid-cols-\[2rem_1fr_2rem\] > button,
button.touch-none {
background: var(--m3-secondary-container) !important;
border: 1px solid color-mix(in srgb, var(--m3-secondary) 28%, var(--m3-outline-variant)) !important;
color: var(--m3-on-secondary-container) !important;
border-radius: 999px !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
letter-spacing: 0 !important;
min-height: 42px;
}
.grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > button:hover,
.grid.grid-cols-\[2rem_1fr_2rem\] > button:hover,
button.touch-none:hover {
background: color-mix(in srgb, var(--m3-secondary-container) 82%, var(--m3-primary) 18%) !important;
color: var(--m3-on-secondary-container) !important;
}
.grid.grid-cols-\[2rem_1fr_2rem\] > input,
button.touch-none,
.grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > div {
background: var(--m3-surface-container-lowest) !important;
border-color: var(--m3-soft-outline) !important;
}
.grid.grid-cols-\[2rem_1fr_2rem\] > input,
button.touch-none span.font-mono,
.grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] span.font-mono,
.grid.grid-cols-2.gap-2 span.font-mono {
color: var(--m3-on-surface) !important;
-webkit-text-fill-color: var(--m3-on-surface);
font-weight: 500 !important;
}
.grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] span:not(.font-mono),
.grid.grid-cols-2.gap-2 span:not(.font-mono) {
font-weight: 400 !important;
}
.grid.grid-cols-\[2rem_1fr_2rem\] > input {
text-align: center;
font-size: 0.95rem !important;
min-height: 42px;
}
.grid.grid-cols-\[2rem_1fr_2rem\] > input:focus {
border-color: var(--m3-primary) !important;
box-shadow: 0 0 0 3px var(--m3-primary-state) !important;
}
#app .font-bold,
#app .font-semibold,
#app strong,
#app b {
font-weight: 500 !important;
}
#app label,
#app summary,
#app p,
#app span {
letter-spacing: 0 !important;
}
#app section .flex.flex-wrap.gap-1.mb-2 > div,
#app section .grid.grid-cols-2.gap-2 > div,
#app section .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > div,
#app section button.touch-none {
background: var(--m3-surface-container-lowest) !important;
border-color: var(--m3-soft-outline) !important;
color: var(--m3-on-surface) !important;
}
#app section .flex.flex-wrap.gap-1.mb-2 > div span,
#app section .grid.grid-cols-2.gap-2 > div span,
#app section .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > div span,
#app section button.touch-none span {
color: var(--m3-on-surface) !important;
-webkit-text-fill-color: var(--m3-on-surface);
font-weight: 500 !important;
}
#app section .flex.flex-wrap.gap-1.mb-2 > div span:first-child,
#app section .grid.grid-cols-2.gap-2 > div span:first-child,
#app section .grid.grid-cols-\[3\.25rem_1fr_3\.25rem\] > div span:first-child,
#app section button.touch-none span:first-child {
color: var(--m3-on-surface-variant) !important;
-webkit-text-fill-color: var(--m3-on-surface-variant);
font-weight: 400 !important;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+23 -1
View File
@@ -5,7 +5,16 @@ import unittest
from fastapi import HTTPException, UploadFile
from fastapi.responses import JSONResponse
from dea_api import BodeParams, DesignParams, add_security_headers, calculate_bode, design_filter, filter_csv
from dea_api import (
BodeCompareParams,
BodeParams,
DesignParams,
add_security_headers,
calculate_bode,
calculate_bode_compare,
design_filter,
filter_csv,
)
class DeaApiTest(unittest.TestCase):
@@ -65,6 +74,19 @@ class DeaApiTest(unittest.TestCase):
raise AssertionError("Expected a[0] validation to fail")
def test_bode_compare_reuses_one_frequency_axis(self):
body = calculate_bode_compare(
BodeCompareParams(
ideal=BodeParams(fs=1000, b=[1.0], a=[1.0]),
fixed=BodeParams(fs=1000, b=[0.5, 0.5], a=[1.0]),
)
)
self.assertEqual(len(body["freq"]), len(body["ideal"]["mag"]))
self.assertEqual(len(body["freq"]), len(body["fixed"]["mag"]))
self.assertEqual(len(body["ideal"]["phase"]), len(body["fixed"]["phase"]))
def test_filter_downsamples_plot_response_for_large_csv(self):
rows = ["value"] + [str(i) for i in range(6001)]
upload = UploadFile(