Spaces:
Sleeping
Sleeping
Sync from GitHub refs/heads/main @ fa80e51
Browse files- Dockerfile +7 -4
- config/models.yaml +9 -1
- pyproject.toml +1 -0
- src/main.py +13 -5
- src/providers/huggingface.py +52 -1
- src/services/trend_predictor.py +196 -7
- uv.lock +152 -0
Dockerfile
CHANGED
|
@@ -38,11 +38,14 @@ ENV PORT=7860
|
|
| 38 |
ENV HF_HOME=/tmp/hf_cache
|
| 39 |
ENV TORCH_HOME=/tmp/torch_cache
|
| 40 |
ENV TRANSFORMERS_OFFLINE=0
|
| 41 |
-
# Production warm-start — preload chat (gemma-4-e4b)
|
| 42 |
-
# first /chat request doesn't pay
|
| 43 |
-
# (opt-in) because Apple Silicon
|
| 44 |
-
# cohabit MPS unified memory; HF Spaces L40S
|
|
|
|
|
|
|
| 45 |
ENV ML_PRELOAD_CHAT=1
|
|
|
|
| 46 |
|
| 47 |
EXPOSE 7860
|
| 48 |
USER appuser
|
|
|
|
| 38 |
ENV HF_HOME=/tmp/hf_cache
|
| 39 |
ENV TORCH_HOME=/tmp/torch_cache
|
| 40 |
ENV TRANSFORMERS_OFFLINE=0
|
| 41 |
+
# Production warm-start — preload chat (gemma-4-e4b) + trend_predict (TimesFM 2.5)
|
| 42 |
+
# at lifespan boot so first /chat or /api/v1/ml/trends/predict request doesn't pay
|
| 43 |
+
# ~30s / ~10s cold-load. Defaults in main.py are lazy (opt-in) because Apple Silicon
|
| 44 |
+
# dev hangs when Gemma + FashionSigLIP cohabit MPS unified memory; HF Spaces L40S
|
| 45 |
+
# has no such constraint. TimesFM 2.5 also requires .compile(ForecastConfig) which
|
| 46 |
+
# runs at first load — preload absorbs that one-time JIT cost.
|
| 47 |
ENV ML_PRELOAD_CHAT=1
|
| 48 |
+
ENV ML_PRELOAD_TIMESFM=1
|
| 49 |
|
| 50 |
EXPOSE 7860
|
| 51 |
USER appuser
|
config/models.yaml
CHANGED
|
@@ -33,6 +33,13 @@ models:
|
|
| 33 |
provider: huggingface
|
| 34 |
model_id: "amazon/chronos-bolt-tiny"
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
xgboost-valuation:
|
| 37 |
provider: xgboost
|
| 38 |
model_path: "models/valuation_xgb.joblib"
|
|
@@ -181,7 +188,8 @@ capabilities:
|
|
| 181 |
min_samples: 50
|
| 182 |
|
| 183 |
trend_predict:
|
| 184 |
-
|
|
|
|
| 185 |
dev_mode: "${TREND_DEV_MODE:-false}"
|
| 186 |
|
| 187 |
size_recommend:
|
|
|
|
| 33 |
provider: huggingface
|
| 34 |
model_id: "amazon/chronos-bolt-tiny"
|
| 35 |
|
| 36 |
+
# TimesFM 2.5:Google 200M 參數,Apache 2.0,univariate forecasting
|
| 37 |
+
# 取代 chronos-bolt-tiny(9M)做主力時序預測。chronos-bolt 保留 entry
|
| 38 |
+
# 給 TREND_MODEL=chronos-bolt 回退(git-bisect / smoke prod issue)。
|
| 39 |
+
timesfm-2.5:
|
| 40 |
+
provider: huggingface
|
| 41 |
+
model_id: "google/timesfm-2.5-200m-pytorch"
|
| 42 |
+
|
| 43 |
xgboost-valuation:
|
| 44 |
provider: xgboost
|
| 45 |
model_path: "models/valuation_xgb.joblib"
|
|
|
|
| 188 |
min_samples: 50
|
| 189 |
|
| 190 |
trend_predict:
|
| 191 |
+
# ${TREND_MODEL:-timesfm-2.5}:預設 TimesFM 2.5;設 TREND_MODEL=chronos-bolt 可回退舊 9M tiny。
|
| 192 |
+
model: "${TREND_MODEL:-timesfm-2.5}"
|
| 193 |
dev_mode: "${TREND_DEV_MODE:-false}"
|
| 194 |
|
| 195 |
size_recommend:
|
pyproject.toml
CHANGED
|
@@ -19,6 +19,7 @@ dependencies = [
|
|
| 19 |
"scikit-learn>=1.4.0",
|
| 20 |
"joblib>=1.3.0",
|
| 21 |
"chronos-forecasting>=2.0.0",
|
|
|
|
| 22 |
"pytrends>=4.9.0",
|
| 23 |
"timm>=1.0.25",
|
| 24 |
"einops>=0.8.2",
|
|
|
|
| 19 |
"scikit-learn>=1.4.0",
|
| 20 |
"joblib>=1.3.0",
|
| 21 |
"chronos-forecasting>=2.0.0",
|
| 22 |
+
"timesfm>=1.3.0",
|
| 23 |
"pytrends>=4.9.0",
|
| 24 |
"timm>=1.0.25",
|
| 25 |
"einops>=0.8.2",
|
src/main.py
CHANGED
|
@@ -74,7 +74,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
|
| 74 |
except Exception:
|
| 75 |
pass # Non-fatal: will lazy-load on first request
|
| 76 |
|
| 77 |
-
# 3. 背景預載
|
| 78 |
# 載入完成前 health endpoint 仍可回應,model 相關 endpoint 會等 lock 釋放
|
| 79 |
#
|
| 80 |
# 預設 lazy(不預載)— Gemma 4 ~7GB 與 FashionSigLIP 共住在 Apple
|
|
@@ -83,15 +83,23 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
|
| 83 |
# /chat 等請求才拉權重;只 SigLIP 工作流(backfill / paste / embed)
|
| 84 |
# 永遠不會觸發 cohabitation。
|
| 85 |
#
|
| 86 |
-
#
|
| 87 |
-
# 第一個聊天請求
|
|
|
|
|
|
|
|
|
|
| 88 |
if os.environ.get("ML_PRELOAD_CHAT") == "1":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
async def _preload_critical_models() -> None:
|
| 91 |
from src.registry import get_registry
|
| 92 |
|
| 93 |
reg = get_registry()
|
| 94 |
-
for cap in
|
| 95 |
try:
|
| 96 |
await reg.get(cap)
|
| 97 |
# WARN level so HF Spaces / Render container logs surface
|
|
@@ -123,7 +131,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
|
| 123 |
else:
|
| 124 |
# WARN level — see Pre-loaded note above re: root logger default.
|
| 125 |
_logger.warning(
|
| 126 |
-
"ml-service startup:
|
| 127 |
)
|
| 128 |
|
| 129 |
yield # 應用程式運行中
|
|
|
|
| 74 |
except Exception:
|
| 75 |
pass # Non-fatal: will lazy-load on first request
|
| 76 |
|
| 77 |
+
# 3. 背景預載 capability,不阻塞 server 啟動
|
| 78 |
# 載入完成前 health endpoint 仍可回應,model 相關 endpoint 會等 lock 釋放
|
| 79 |
#
|
| 80 |
# 預設 lazy(不預載)— Gemma 4 ~7GB 與 FashionSigLIP 共住在 Apple
|
|
|
|
| 83 |
# /chat 等請求才拉權重;只 SigLIP 工作流(backfill / paste / embed)
|
| 84 |
# 永遠不會觸發 cohabitation。
|
| 85 |
#
|
| 86 |
+
# Production 預載(HF Spaces L40S):
|
| 87 |
+
# - ML_PRELOAD_CHAT=1 → 預載 chat (gemma-4-e4b),避免第一個聊天請求 ~30s 冷啟
|
| 88 |
+
# - ML_PRELOAD_TIMESFM=1 → 預載 trend_predict (TimesFM 2.5),避免第一個趨勢請求
|
| 89 |
+
# ~10s 冷啟 + ForecastConfig.compile() 開銷
|
| 90 |
+
_preload_caps: list[str] = []
|
| 91 |
if os.environ.get("ML_PRELOAD_CHAT") == "1":
|
| 92 |
+
_preload_caps.append("chat")
|
| 93 |
+
if os.environ.get("ML_PRELOAD_TIMESFM") == "1":
|
| 94 |
+
_preload_caps.append("trend_predict")
|
| 95 |
+
|
| 96 |
+
if _preload_caps:
|
| 97 |
|
| 98 |
async def _preload_critical_models() -> None:
|
| 99 |
from src.registry import get_registry
|
| 100 |
|
| 101 |
reg = get_registry()
|
| 102 |
+
for cap in _preload_caps:
|
| 103 |
try:
|
| 104 |
await reg.get(cap)
|
| 105 |
# WARN level so HF Spaces / Render container logs surface
|
|
|
|
| 131 |
else:
|
| 132 |
# WARN level — see Pre-loaded note above re: root logger default.
|
| 133 |
_logger.warning(
|
| 134 |
+
"ml-service startup: no ML_PRELOAD_* set — all capabilities lazy-load on first request"
|
| 135 |
)
|
| 136 |
|
| 137 |
yield # 應用程式運行中
|
src/providers/huggingface.py
CHANGED
|
@@ -75,10 +75,14 @@ class HuggingFaceProvider(ModelProvider):
|
|
| 75 |
if "florence" in model_id.lower():
|
| 76 |
return await self._load_florence(parsed)
|
| 77 |
|
| 78 |
-
# Chronos-Bolt(時序預測)
|
| 79 |
if "chronos" in model_id.lower():
|
| 80 |
return await self._load_chronos(parsed)
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
# 預設:Causal LM(Qwen 系列)+ optional LoRA
|
| 83 |
return await self._load_causal_lm(parsed)
|
| 84 |
|
|
@@ -359,6 +363,53 @@ class HuggingFaceProvider(ModelProvider):
|
|
| 359 |
config={"model_id": config.model_id, "device": device},
|
| 360 |
)
|
| 361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
@staticmethod
|
| 363 |
def _resolve_device(device_str: str) -> str:
|
| 364 |
"""解析裝置字串,auto 會偵測 cuda > mps > cpu 順序。"""
|
|
|
|
| 75 |
if "florence" in model_id.lower():
|
| 76 |
return await self._load_florence(parsed)
|
| 77 |
|
| 78 |
+
# Chronos-Bolt(時序預測,舊;保留給 TREND_MODEL=chronos-bolt 回退)
|
| 79 |
if "chronos" in model_id.lower():
|
| 80 |
return await self._load_chronos(parsed)
|
| 81 |
|
| 82 |
+
# TimesFM 2.5(Google 200M 時序基礎模型,Apache 2.0)
|
| 83 |
+
if "timesfm" in model_id.lower():
|
| 84 |
+
return await self._load_timesfm(parsed)
|
| 85 |
+
|
| 86 |
# 預設:Causal LM(Qwen 系列)+ optional LoRA
|
| 87 |
return await self._load_causal_lm(parsed)
|
| 88 |
|
|
|
|
| 363 |
config={"model_id": config.model_id, "device": device},
|
| 364 |
)
|
| 365 |
|
| 366 |
+
async def _load_timesfm(self, config: HFModelConfig) -> LoadedModel:
|
| 367 |
+
"""載入 TimesFM 2.5 — 在 worker thread 中執行避免阻塞 event loop。"""
|
| 368 |
+
return await asyncio.to_thread(self._load_timesfm_sync, config)
|
| 369 |
+
|
| 370 |
+
def _load_timesfm_sync(self, config: HFModelConfig) -> LoadedModel:
|
| 371 |
+
"""同步載入 Google TimesFM 2.5 200M(由 worker thread 呼叫)。
|
| 372 |
+
|
| 373 |
+
【為什麼需要 .compile()?】
|
| 374 |
+
TimesFM 2.5 的 from_pretrained 只 load weights,inference 前必須先 compile
|
| 375 |
+
ForecastConfig — 否則 forecast() 會 RuntimeError。compile 一次後可重用。
|
| 376 |
+
|
| 377 |
+
【ForecastConfig 參數選擇】
|
| 378 |
+
- max_horizon=128:90 天足夠,留 buffer 給未來 180 天 endpoint 不需重 compile
|
| 379 |
+
- max_context=1024:歷史價格序列上限,遠超 price_history 實際需求
|
| 380 |
+
- normalize_inputs:消除品牌間絕對價差影響
|
| 381 |
+
- use_continuous_quantile_head + fix_quantile_crossing:取得單調遞增 quantile
|
| 382 |
+
- infer_is_positive:價格非負強制 forecast≥0
|
| 383 |
+
- force_flip_invariance:抗 sign-flip pretraining artifact
|
| 384 |
+
"""
|
| 385 |
+
import torch
|
| 386 |
+
import timesfm
|
| 387 |
+
|
| 388 |
+
device = self._resolve_device(config.device)
|
| 389 |
+
logger.info("Loading TimesFM 2.5 (%s) on %s", config.model_id, device)
|
| 390 |
+
|
| 391 |
+
torch.set_float32_matmul_precision("high")
|
| 392 |
+
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(config.model_id)
|
| 393 |
+
model.compile(
|
| 394 |
+
timesfm.ForecastConfig(
|
| 395 |
+
max_context=1024,
|
| 396 |
+
max_horizon=128,
|
| 397 |
+
normalize_inputs=True,
|
| 398 |
+
use_continuous_quantile_head=True,
|
| 399 |
+
force_flip_invariance=True,
|
| 400 |
+
infer_is_positive=True,
|
| 401 |
+
fix_quantile_crossing=True,
|
| 402 |
+
)
|
| 403 |
+
)
|
| 404 |
+
self._loaded = True
|
| 405 |
+
logger.info("TimesFM 2.5 loaded + compiled successfully")
|
| 406 |
+
|
| 407 |
+
return LoadedModel(
|
| 408 |
+
model=model,
|
| 409 |
+
tokenizer=None,
|
| 410 |
+
config={"model_id": config.model_id, "device": device},
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
@staticmethod
|
| 414 |
def _resolve_device(device_str: str) -> str:
|
| 415 |
"""解析裝置字串,auto 會偵測 cuda > mps > cpu 順序。"""
|
src/services/trend_predictor.py
CHANGED
|
@@ -38,10 +38,30 @@ logger = logging.getLogger(__name__)
|
|
| 38 |
# ============================================================================
|
| 39 |
# 模式切換
|
| 40 |
# ============================================================================
|
| 41 |
-
# 預設 false = 生產模式(用真實
|
| 42 |
# 設 true = 開發模式(回傳假資料,不打外部 API)
|
| 43 |
DEV_MODE = os.environ.get("TREND_DEV_MODE", "false").lower() == "true"
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
# ============================================================================
|
| 46 |
# ModelRegistry 整合
|
| 47 |
# ============================================================================
|
|
@@ -99,6 +119,10 @@ class TrendPrediction(BaseModel):
|
|
| 99 |
predicted_price_90d: float # 90 天後的預測均價
|
| 100 |
google_trend_score: float # Google Trends 熱度分數(0-100)
|
| 101 |
forecast_generated_at: str # 預測產生時間(ISO 格式)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
# ============================================================================
|
|
@@ -182,14 +206,22 @@ def predict_trend(
|
|
| 182 |
參數:
|
| 183 |
brand: 品牌名(如 "Gucci")
|
| 184 |
price_history: 歷史成交紀錄,格式 [{"date": "2024-01-15", "price": 1200.0}, ...]
|
| 185 |
-
可選——沒有的話,
|
| 186 |
|
| 187 |
回傳:
|
| 188 |
-
TrendPrediction — 含 30d/90d 預測價、趨勢方向、Google Trends 分數
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
"""
|
| 190 |
if DEV_MODE:
|
| 191 |
return _mock_predict(brand)
|
| 192 |
-
|
|
|
|
|
|
|
| 193 |
|
| 194 |
|
| 195 |
# ============================================================================
|
|
@@ -197,15 +229,22 @@ def predict_trend(
|
|
| 197 |
# ============================================================================
|
| 198 |
|
| 199 |
|
| 200 |
-
def _mock_predict(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
"""
|
| 202 |
-
開發模式:用硬編碼的基準價 + 趨勢方向,模擬預測結果。
|
| 203 |
|
| 204 |
不打任何外部 API,適合離線開發和 CI 測試。
|
| 205 |
價格變化邏輯:
|
| 206 |
- UP: 30d +3%, 90d +8%
|
| 207 |
- DOWN: 30d -3%, 90d -8%
|
| 208 |
- STABLE: 30d +0.5%, 90d +1%
|
|
|
|
|
|
|
|
|
|
| 209 |
"""
|
| 210 |
baseline = _BRAND_BASELINES.get(brand, 500.0)
|
| 211 |
direction = _MOCK_DIRECTIONS.get(brand, TrendDirection.STABLE)
|
|
@@ -228,6 +267,8 @@ def _mock_predict(brand: str) -> TrendPrediction:
|
|
| 228 |
predicted_price_90d=price_90d,
|
| 229 |
google_trend_score=trend_score,
|
| 230 |
forecast_generated_at=datetime.now(tz=timezone.utc).isoformat(),
|
|
|
|
|
|
|
| 231 |
)
|
| 232 |
|
| 233 |
|
|
@@ -393,4 +434,152 @@ def _chronos_predict(
|
|
| 393 |
# 任何錯誤(模型載入失敗、tensor 格式錯誤等)都 fallback 到 mock
|
| 394 |
# 確保 API 不會因為預測失敗而整個掛掉
|
| 395 |
logger.exception("Chronos prediction failed for brand=%s", brand)
|
| 396 |
-
return _mock_predict(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# ============================================================================
|
| 39 |
# 模式切換
|
| 40 |
# ============================================================================
|
| 41 |
+
# 預設 false = 生產模式(用真實 TimesFM + Google Trends)
|
| 42 |
# 設 true = 開發模式(回傳假資料,不打外部 API)
|
| 43 |
DEV_MODE = os.environ.get("TREND_DEV_MODE", "false").lower() == "true"
|
| 44 |
|
| 45 |
+
# 主力模型切換:預設 TimesFM 2.5(200M, Apache 2.0)
|
| 46 |
+
# 設 TREND_MODEL=chronos-bolt 可回退舊 chronos-bolt-tiny(9M)做 git-bisect
|
| 47 |
+
TREND_MODEL = os.environ.get("TREND_MODEL", "timesfm-2.5").lower()
|
| 48 |
+
|
| 49 |
+
# TimesFM 3-gate 信心門檻
|
| 50 |
+
# n<32: 純規則 fallback(mock + low_confidence)
|
| 51 |
+
# 32 ≤ n < 100: TimesFM + low_confidence flag
|
| 52 |
+
# n ≥ 100: TimesFM 全信心
|
| 53 |
+
TIMESFM_LOW_CONFIDENCE_MIN = 32
|
| 54 |
+
TIMESFM_FULL_CONFIDENCE_MIN = 100
|
| 55 |
+
|
| 56 |
+
_DISCLAIMER_INSUFFICIENT = (
|
| 57 |
+
"Insufficient price history (<32 records) — forecast uses brand baseline "
|
| 58 |
+
"rules, not the TimesFM model."
|
| 59 |
+
)
|
| 60 |
+
_DISCLAIMER_LIMITED = (
|
| 61 |
+
"Limited price history (32-100 records) — TimesFM forecast may have wider "
|
| 62 |
+
"uncertainty."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
# ============================================================================
|
| 66 |
# ModelRegistry 整合
|
| 67 |
# ============================================================================
|
|
|
|
| 119 |
predicted_price_90d: float # 90 天後的預測均價
|
| 120 |
google_trend_score: float # Google Trends 熱度分數(0-100)
|
| 121 |
forecast_generated_at: str # 預測產生時間(ISO 格式)
|
| 122 |
+
# ── 新增:信心揭露(PR-2 TimesFM 2.5 swap,additive 不破壞既有 caller)──
|
| 123 |
+
# low_confidence=true 時前端應顯示 disclaimer(n<100 或 fallback)
|
| 124 |
+
low_confidence: bool = False
|
| 125 |
+
disclaimer: str = ""
|
| 126 |
|
| 127 |
|
| 128 |
# ============================================================================
|
|
|
|
| 206 |
參數:
|
| 207 |
brand: 品牌名(如 "Gucci")
|
| 208 |
price_history: 歷史成交紀錄,格式 [{"date": "2024-01-15", "price": 1200.0}, ...]
|
| 209 |
+
可選——沒有的話,model 會 fallback 到 mock 價格 + 真實 Google Trends
|
| 210 |
|
| 211 |
回傳:
|
| 212 |
+
TrendPrediction — 含 30d/90d 預測價、趨勢方向、Google Trends 分數、
|
| 213 |
+
low_confidence flag、disclaimer
|
| 214 |
+
|
| 215 |
+
分派邏輯:
|
| 216 |
+
- DEV_MODE=true → mock(不打外部 API)
|
| 217 |
+
- TREND_MODEL=chronos-bolt → 舊 Chronos-Bolt-Tiny(git-bisect rollback path)
|
| 218 |
+
- 預設 TREND_MODEL=timesfm-2.5 → Google TimesFM 2.5 200M(3-gate 信心邏輯)
|
| 219 |
"""
|
| 220 |
if DEV_MODE:
|
| 221 |
return _mock_predict(brand)
|
| 222 |
+
if TREND_MODEL == "chronos-bolt":
|
| 223 |
+
return _chronos_predict(brand, price_history)
|
| 224 |
+
return _timesfm_predict(brand, price_history)
|
| 225 |
|
| 226 |
|
| 227 |
# ============================================================================
|
|
|
|
| 229 |
# ============================================================================
|
| 230 |
|
| 231 |
|
| 232 |
+
def _mock_predict(
|
| 233 |
+
brand: str,
|
| 234 |
+
low_confidence: bool = False,
|
| 235 |
+
disclaimer: str = "",
|
| 236 |
+
) -> TrendPrediction:
|
| 237 |
"""
|
| 238 |
+
開發模式 / fallback:用硬編碼的基準價 + 趨勢方向,模擬預測結果。
|
| 239 |
|
| 240 |
不打任何外部 API,適合離線開發和 CI 測試。
|
| 241 |
價格變化邏輯:
|
| 242 |
- UP: 30d +3%, 90d +8%
|
| 243 |
- DOWN: 30d -3%, 90d -8%
|
| 244 |
- STABLE: 30d +0.5%, 90d +1%
|
| 245 |
+
|
| 246 |
+
當生產 path 因資料不足/模型失敗 fallback 到此函式時,caller 應傳
|
| 247 |
+
low_confidence=True + disclaimer 字串,讓前端能正確標示。
|
| 248 |
"""
|
| 249 |
baseline = _BRAND_BASELINES.get(brand, 500.0)
|
| 250 |
direction = _MOCK_DIRECTIONS.get(brand, TrendDirection.STABLE)
|
|
|
|
| 267 |
predicted_price_90d=price_90d,
|
| 268 |
google_trend_score=trend_score,
|
| 269 |
forecast_generated_at=datetime.now(tz=timezone.utc).isoformat(),
|
| 270 |
+
low_confidence=low_confidence,
|
| 271 |
+
disclaimer=disclaimer,
|
| 272 |
)
|
| 273 |
|
| 274 |
|
|
|
|
| 434 |
# 任何錯誤(模型載入失敗、tensor 格式錯誤等)都 fallback 到 mock
|
| 435 |
# 確保 API 不會因為預測失敗而整個掛掉
|
| 436 |
logger.exception("Chronos prediction failed for brand=%s", brand)
|
| 437 |
+
return _mock_predict(
|
| 438 |
+
brand,
|
| 439 |
+
low_confidence=True,
|
| 440 |
+
disclaimer=_DISCLAIMER_INSUFFICIENT,
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
# ============================================================================
|
| 445 |
+
# 生���模式:TimesFM 2.5(主力)
|
| 446 |
+
# ============================================================================
|
| 447 |
+
# Google Research 200M 參數 decoder-only 時序基礎模型,Apache 2.0。
|
| 448 |
+
# 取代 chronos-bolt-tiny(9M)作為主力預測。
|
| 449 |
+
|
| 450 |
+
TIMESFM_MODEL_ID = "google/timesfm-2.5-200m-pytorch"
|
| 451 |
+
|
| 452 |
+
# Lazy loading:第一次呼叫載入 + compile,之後重複使用
|
| 453 |
+
_timesfm_model = None
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def _load_timesfm():
|
| 457 |
+
"""
|
| 458 |
+
載入並 compile TimesFM 2.5。第一次呼叫後快取於全域變數。
|
| 459 |
+
|
| 460 |
+
【為什麼要 .compile()?】
|
| 461 |
+
TimesFM 2.5 from_pretrained 只 load weights;inference 前必須先 compile
|
| 462 |
+
ForecastConfig — 否則 forecast() 會 RuntimeError。compile 一次後可重用。
|
| 463 |
+
|
| 464 |
+
【ForecastConfig 參數】
|
| 465 |
+
max_horizon=128:90d 足夠,buffer 留 180d endpoint 不需重 compile
|
| 466 |
+
max_context=1024:歷史價格序列上限,遠超實際 use case
|
| 467 |
+
normalize_inputs:消除品牌間絕對價差影響
|
| 468 |
+
use_continuous_quantile_head + fix_quantile_crossing:取得單調遞增 quantile
|
| 469 |
+
infer_is_positive:價格非負強制 forecast≥0
|
| 470 |
+
force_flip_invariance:抗 sign-flip pretraining artifact
|
| 471 |
+
"""
|
| 472 |
+
global _timesfm_model
|
| 473 |
+
if _timesfm_model is not None:
|
| 474 |
+
return _timesfm_model
|
| 475 |
+
|
| 476 |
+
import timesfm
|
| 477 |
+
|
| 478 |
+
logger.info("Loading TimesFM 2.5 model (%s)...", TIMESFM_MODEL_ID)
|
| 479 |
+
torch.set_float32_matmul_precision("high")
|
| 480 |
+
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(TIMESFM_MODEL_ID)
|
| 481 |
+
model.compile(
|
| 482 |
+
timesfm.ForecastConfig(
|
| 483 |
+
max_context=1024,
|
| 484 |
+
max_horizon=128,
|
| 485 |
+
normalize_inputs=True,
|
| 486 |
+
use_continuous_quantile_head=True,
|
| 487 |
+
force_flip_invariance=True,
|
| 488 |
+
infer_is_positive=True,
|
| 489 |
+
fix_quantile_crossing=True,
|
| 490 |
+
)
|
| 491 |
+
)
|
| 492 |
+
_timesfm_model = model
|
| 493 |
+
logger.info("TimesFM 2.5 loaded + compiled on %s", DEVICE)
|
| 494 |
+
return _timesfm_model
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def _timesfm_predict(
|
| 498 |
+
brand: str, price_history: list[dict[str, float | str]] | None = None
|
| 499 |
+
) -> TrendPrediction:
|
| 500 |
+
"""
|
| 501 |
+
用 TimesFM 2.5 做 zero-shot 時序預測,3-gate 信心邏輯。
|
| 502 |
+
|
| 503 |
+
【3-gate】
|
| 504 |
+
n<32:純規則 fallback(mock baseline + low_confidence + disclaimer)
|
| 505 |
+
32 ≤ n < 100:TimesFM + low_confidence=True
|
| 506 |
+
n ≥ 100:TimesFM 全信心(low_confidence=False)
|
| 507 |
+
|
| 508 |
+
【相比 Chronos-Bolt-Tiny 的改進】
|
| 509 |
+
- 200M params vs 9M:對長序列關係建模能力大幅提升
|
| 510 |
+
- 連續 quantile head:信賴區間連續而非離散
|
| 511 |
+
- flip-invariance + is_positive 約束:價格序列特殊化處理
|
| 512 |
+
- max_context=1024:可吃更長歷史不需 truncate
|
| 513 |
+
"""
|
| 514 |
+
try:
|
| 515 |
+
# Step 1: 即時抓 Google Trends 分數(與 Chronos path 行為一致)
|
| 516 |
+
google_score = _fetch_google_trend_score(brand)
|
| 517 |
+
n = len(price_history) if price_history else 0
|
| 518 |
+
|
| 519 |
+
# Step 2: n<32 純規則 fallback
|
| 520 |
+
if n < TIMESFM_LOW_CONFIDENCE_MIN:
|
| 521 |
+
mock = _mock_predict(
|
| 522 |
+
brand,
|
| 523 |
+
low_confidence=True,
|
| 524 |
+
disclaimer=_DISCLAIMER_INSUFFICIENT,
|
| 525 |
+
)
|
| 526 |
+
mock.google_trend_score = google_score
|
| 527 |
+
return mock
|
| 528 |
+
|
| 529 |
+
# Step 3: 準備時間序列(按日期排序,取價格 1-D array)
|
| 530 |
+
# n<32 gate 保證 price_history 非 None;此 raise 是 unreachable 防禦,
|
| 531 |
+
# 但比 `assert price_history is not None` 安全:python -O 會 strip
|
| 532 |
+
# asserts,只剩此 explicit raise 才能 narrow 型別 + 保護 None.sorted()
|
| 533 |
+
# 跑進 except 變成 silent fallback。
|
| 534 |
+
if price_history is None:
|
| 535 |
+
raise ValueError("price_history must be non-None past n<32 gate")
|
| 536 |
+
sorted_history = sorted(price_history, key=lambda x: str(x["date"]))
|
| 537 |
+
prices = [float(p["price"]) for p in sorted_history]
|
| 538 |
+
|
| 539 |
+
# Step 4: TimesFM forecast
|
| 540 |
+
# inputs 接受 list of 1-D arrays,回傳 (point_forecast=median, quantile_forecast)
|
| 541 |
+
# point_forecast shape: (batch=1, horizon=90)
|
| 542 |
+
# 用 numpy float32 直接傳入(避免 torch tensor 型別不符)
|
| 543 |
+
import numpy as np
|
| 544 |
+
|
| 545 |
+
prices_array = np.array(prices, dtype=np.float32)
|
| 546 |
+
model = _load_timesfm()
|
| 547 |
+
point_forecast, _quantile_forecast = model.forecast(
|
| 548 |
+
horizon=90,
|
| 549 |
+
inputs=[prices_array],
|
| 550 |
+
)
|
| 551 |
+
pred_30d = float(point_forecast[0, 29]) # 第 30 天
|
| 552 |
+
pred_90d = float(point_forecast[0, 89]) # 第 90 天
|
| 553 |
+
|
| 554 |
+
# Step 5: 趨勢方向(與 Chronos path 邏輯一致)
|
| 555 |
+
current_price = prices[-1]
|
| 556 |
+
if pred_90d > current_price * 1.05:
|
| 557 |
+
direction = TrendDirection.UP
|
| 558 |
+
elif pred_90d < current_price * 0.95:
|
| 559 |
+
direction = TrendDirection.DOWN
|
| 560 |
+
else:
|
| 561 |
+
direction = TrendDirection.STABLE
|
| 562 |
+
|
| 563 |
+
# Step 6: 信心 flag
|
| 564 |
+
low_confidence = n < TIMESFM_FULL_CONFIDENCE_MIN
|
| 565 |
+
disclaimer = _DISCLAIMER_LIMITED if low_confidence else ""
|
| 566 |
+
|
| 567 |
+
return TrendPrediction(
|
| 568 |
+
brand=brand,
|
| 569 |
+
trend_direction=direction,
|
| 570 |
+
predicted_price_30d=round(pred_30d, 2),
|
| 571 |
+
predicted_price_90d=round(pred_90d, 2),
|
| 572 |
+
google_trend_score=google_score,
|
| 573 |
+
forecast_generated_at=datetime.now(tz=timezone.utc).isoformat(),
|
| 574 |
+
low_confidence=low_confidence,
|
| 575 |
+
disclaimer=disclaimer,
|
| 576 |
+
)
|
| 577 |
+
except Exception:
|
| 578 |
+
# 任何錯誤(模型載入失敗、numpy 格式錯誤等)fallback 到 mock + low_confidence
|
| 579 |
+
# 確保 API 不會因為預測失敗而整個掛掉
|
| 580 |
+
logger.exception("TimesFM prediction failed for brand=%s", brand)
|
| 581 |
+
return _mock_predict(
|
| 582 |
+
brand,
|
| 583 |
+
low_confidence=True,
|
| 584 |
+
disclaimer=_DISCLAIMER_INSUFFICIENT,
|
| 585 |
+
)
|
uv.lock
CHANGED
|
@@ -19,6 +19,15 @@ resolution-markers = [
|
|
| 19 |
[manifest]
|
| 20 |
overrides = [{ name = "transformers", specifier = ">=5.5.0" }]
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
[[package]]
|
| 23 |
name = "accelerate"
|
| 24 |
version = "1.13.0"
|
|
@@ -549,6 +558,19 @@ wheels = [
|
|
| 549 |
{ url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" },
|
| 550 |
]
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
[[package]]
|
| 553 |
name = "fastapi"
|
| 554 |
version = "0.135.1"
|
|
@@ -705,6 +727,30 @@ wheels = [
|
|
| 705 |
{ url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" },
|
| 706 |
]
|
| 707 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
[[package]]
|
| 709 |
name = "google-api-core"
|
| 710 |
version = "2.30.0"
|
|
@@ -1327,6 +1373,15 @@ wheels = [
|
|
| 1327 |
{ url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" },
|
| 1328 |
]
|
| 1329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1330 |
[[package]]
|
| 1331 |
name = "networkx"
|
| 1332 |
version = "3.6.1"
|
|
@@ -1746,6 +1801,15 @@ wheels = [
|
|
| 1746 |
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
|
| 1747 |
]
|
| 1748 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1749 |
[[package]]
|
| 1750 |
name = "pluggy"
|
| 1751 |
version = "1.6.0"
|
|
@@ -2646,6 +2710,19 @@ wheels = [
|
|
| 2646 |
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
| 2647 |
]
|
| 2648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2649 |
[[package]]
|
| 2650 |
name = "setuptools"
|
| 2651 |
version = "82.0.1"
|
|
@@ -2673,6 +2750,15 @@ wheels = [
|
|
| 2673 |
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
| 2674 |
]
|
| 2675 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2676 |
[[package]]
|
| 2677 |
name = "starlette"
|
| 2678 |
version = "0.52.1"
|
|
@@ -2707,6 +2793,26 @@ wheels = [
|
|
| 2707 |
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
| 2708 |
]
|
| 2709 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2710 |
[[package]]
|
| 2711 |
name = "timm"
|
| 2712 |
version = "1.0.25"
|
|
@@ -2973,6 +3079,21 @@ wheels = [
|
|
| 2973 |
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
| 2974 |
]
|
| 2975 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2976 |
[[package]]
|
| 2977 |
name = "uvicorn"
|
| 2978 |
version = "0.41.0"
|
|
@@ -3035,6 +3156,35 @@ wheels = [
|
|
| 3035 |
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
| 3036 |
]
|
| 3037 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3038 |
[[package]]
|
| 3039 |
name = "wardrobe-os-ml-service"
|
| 3040 |
version = "0.0.1"
|
|
@@ -3062,6 +3212,7 @@ dependencies = [
|
|
| 3062 |
{ name = "pyyaml" },
|
| 3063 |
{ name = "replicate" },
|
| 3064 |
{ name = "scikit-learn" },
|
|
|
|
| 3065 |
{ name = "timm" },
|
| 3066 |
{ name = "torch" },
|
| 3067 |
{ name = "torchvision" },
|
|
@@ -3103,6 +3254,7 @@ requires-dist = [
|
|
| 3103 |
{ name = "pyyaml", specifier = ">=6.0.3" },
|
| 3104 |
{ name = "replicate", specifier = ">=1.0.0" },
|
| 3105 |
{ name = "scikit-learn", specifier = ">=1.4.0" },
|
|
|
|
| 3106 |
{ name = "timm", specifier = ">=1.0.25" },
|
| 3107 |
{ name = "torch", specifier = ">=2.5.0" },
|
| 3108 |
{ name = "torchvision", specifier = ">=0.20.0" },
|
|
|
|
| 19 |
[manifest]
|
| 20 |
overrides = [{ name = "transformers", specifier = ">=5.5.0" }]
|
| 21 |
|
| 22 |
+
[[package]]
|
| 23 |
+
name = "absl-py"
|
| 24 |
+
version = "2.4.0"
|
| 25 |
+
source = { registry = "https://pypi.org/simple" }
|
| 26 |
+
sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" }
|
| 27 |
+
wheels = [
|
| 28 |
+
{ url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" },
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
[[package]]
|
| 32 |
name = "accelerate"
|
| 33 |
version = "1.13.0"
|
|
|
|
| 558 |
{ url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" },
|
| 559 |
]
|
| 560 |
|
| 561 |
+
[[package]]
|
| 562 |
+
name = "einshape"
|
| 563 |
+
version = "1.0"
|
| 564 |
+
source = { registry = "https://pypi.org/simple" }
|
| 565 |
+
dependencies = [
|
| 566 |
+
{ name = "absl-py" },
|
| 567 |
+
{ name = "numpy" },
|
| 568 |
+
]
|
| 569 |
+
sdist = { url = "https://files.pythonhosted.org/packages/cb/c6/95ad0a036656aec1cb32177a5d5abfcfbf53a01c1416484cacb8c7332a84/einshape-1.0.tar.gz", hash = "sha256:53538d75dd099f4ead4a4f786fafdcb0b729bb587e0b3afeca25ceef18c9ac14", size = 14571, upload-time = "2022-12-19T17:09:34.618Z" }
|
| 570 |
+
wheels = [
|
| 571 |
+
{ url = "https://files.pythonhosted.org/packages/71/bb/34cc02f13b438d550e4709216ee1df9da8e55e15b0cc87a2cb5dee19a729/einshape-1.0-py3-none-any.whl", hash = "sha256:42da4c2dea3a27f87ee45a7cee5072a636b97cb184bb07bf5d6412ba0ff7b965", size = 21392, upload-time = "2022-12-19T17:09:32.904Z" },
|
| 572 |
+
]
|
| 573 |
+
|
| 574 |
[[package]]
|
| 575 |
name = "fastapi"
|
| 576 |
version = "0.135.1"
|
|
|
|
| 727 |
{ url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" },
|
| 728 |
]
|
| 729 |
|
| 730 |
+
[[package]]
|
| 731 |
+
name = "gitdb"
|
| 732 |
+
version = "4.0.12"
|
| 733 |
+
source = { registry = "https://pypi.org/simple" }
|
| 734 |
+
dependencies = [
|
| 735 |
+
{ name = "smmap" },
|
| 736 |
+
]
|
| 737 |
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
|
| 738 |
+
wheels = [
|
| 739 |
+
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
|
| 740 |
+
]
|
| 741 |
+
|
| 742 |
+
[[package]]
|
| 743 |
+
name = "gitpython"
|
| 744 |
+
version = "3.1.47"
|
| 745 |
+
source = { registry = "https://pypi.org/simple" }
|
| 746 |
+
dependencies = [
|
| 747 |
+
{ name = "gitdb" },
|
| 748 |
+
]
|
| 749 |
+
sdist = { url = "https://files.pythonhosted.org/packages/c1/bd/50db468e9b1310529a19fce651b3b0e753b5c07954d486cba31bbee9a5d5/gitpython-3.1.47.tar.gz", hash = "sha256:dba27f922bd2b42cb54c87a8ab3cb6beb6bf07f3d564e21ac848913a05a8a3cd", size = 216978, upload-time = "2026-04-22T02:44:44.059Z" }
|
| 750 |
+
wheels = [
|
| 751 |
+
{ url = "https://files.pythonhosted.org/packages/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl", hash = "sha256:489f590edfd6d20571b2c0e72c6a6ac6915ee8b8cd04572330e3842207a78905", size = 209547, upload-time = "2026-04-22T02:44:41.271Z" },
|
| 752 |
+
]
|
| 753 |
+
|
| 754 |
[[package]]
|
| 755 |
name = "google-api-core"
|
| 756 |
version = "2.30.0"
|
|
|
|
| 1373 |
{ url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" },
|
| 1374 |
]
|
| 1375 |
|
| 1376 |
+
[[package]]
|
| 1377 |
+
name = "narwhals"
|
| 1378 |
+
version = "2.20.0"
|
| 1379 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1380 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e9/f3/257adc69a71011b4c8cda321b00f02c5bf1980ae38ffd05a58d9632d4de8/narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e", size = 627848, upload-time = "2026-04-20T12:11:45.427Z" }
|
| 1381 |
+
wheels = [
|
| 1382 |
+
{ url = "https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d", size = 449373, upload-time = "2026-04-20T12:11:43.596Z" },
|
| 1383 |
+
]
|
| 1384 |
+
|
| 1385 |
[[package]]
|
| 1386 |
name = "networkx"
|
| 1387 |
version = "3.6.1"
|
|
|
|
| 1801 |
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
|
| 1802 |
]
|
| 1803 |
|
| 1804 |
+
[[package]]
|
| 1805 |
+
name = "platformdirs"
|
| 1806 |
+
version = "4.9.6"
|
| 1807 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1808 |
+
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
| 1809 |
+
wheels = [
|
| 1810 |
+
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
| 1811 |
+
]
|
| 1812 |
+
|
| 1813 |
[[package]]
|
| 1814 |
name = "pluggy"
|
| 1815 |
version = "1.6.0"
|
|
|
|
| 2710 |
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
| 2711 |
]
|
| 2712 |
|
| 2713 |
+
[[package]]
|
| 2714 |
+
name = "sentry-sdk"
|
| 2715 |
+
version = "2.58.0"
|
| 2716 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2717 |
+
dependencies = [
|
| 2718 |
+
{ name = "certifi" },
|
| 2719 |
+
{ name = "urllib3" },
|
| 2720 |
+
]
|
| 2721 |
+
sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" }
|
| 2722 |
+
wheels = [
|
| 2723 |
+
{ url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
|
| 2724 |
+
]
|
| 2725 |
+
|
| 2726 |
[[package]]
|
| 2727 |
name = "setuptools"
|
| 2728 |
version = "82.0.1"
|
|
|
|
| 2750 |
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
| 2751 |
]
|
| 2752 |
|
| 2753 |
+
[[package]]
|
| 2754 |
+
name = "smmap"
|
| 2755 |
+
version = "5.0.3"
|
| 2756 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2757 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" }
|
| 2758 |
+
wheels = [
|
| 2759 |
+
{ url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
|
| 2760 |
+
]
|
| 2761 |
+
|
| 2762 |
[[package]]
|
| 2763 |
name = "starlette"
|
| 2764 |
version = "0.52.1"
|
|
|
|
| 2793 |
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
| 2794 |
]
|
| 2795 |
|
| 2796 |
+
[[package]]
|
| 2797 |
+
name = "timesfm"
|
| 2798 |
+
version = "1.3.0"
|
| 2799 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2800 |
+
dependencies = [
|
| 2801 |
+
{ name = "absl-py" },
|
| 2802 |
+
{ name = "einshape" },
|
| 2803 |
+
{ name = "huggingface-hub" },
|
| 2804 |
+
{ name = "numpy" },
|
| 2805 |
+
{ name = "pandas" },
|
| 2806 |
+
{ name = "scikit-learn" },
|
| 2807 |
+
{ name = "typer" },
|
| 2808 |
+
{ name = "utilsforecast" },
|
| 2809 |
+
{ name = "wandb" },
|
| 2810 |
+
]
|
| 2811 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1d/38/b98e0e45408348ef91ba8b3035db247200d3ad8a8c8eaf62a82f2add7cf8/timesfm-1.3.0.tar.gz", hash = "sha256:c494775c17d373d8d7f2e5562468fdff371c123d1d6e43dcfe92fbf9a86fec4e", size = 48227, upload-time = "2025-07-06T22:07:07.839Z" }
|
| 2812 |
+
wheels = [
|
| 2813 |
+
{ url = "https://files.pythonhosted.org/packages/d9/04/afb1888ab8db240576811b240cc7b7687c348516a72bfd618c5bd98ea9d2/timesfm-1.3.0-py3-none-any.whl", hash = "sha256:a99e22b13654bdbfaecb14a48e4a8dc6c990b191a0322a5cacd09eb0df3bfebe", size = 55019, upload-time = "2025-07-06T22:07:06.366Z" },
|
| 2814 |
+
]
|
| 2815 |
+
|
| 2816 |
[[package]]
|
| 2817 |
name = "timm"
|
| 2818 |
version = "1.0.25"
|
|
|
|
| 3079 |
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
| 3080 |
]
|
| 3081 |
|
| 3082 |
+
[[package]]
|
| 3083 |
+
name = "utilsforecast"
|
| 3084 |
+
version = "0.2.15"
|
| 3085 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3086 |
+
dependencies = [
|
| 3087 |
+
{ name = "narwhals" },
|
| 3088 |
+
{ name = "numpy" },
|
| 3089 |
+
{ name = "packaging" },
|
| 3090 |
+
{ name = "pandas" },
|
| 3091 |
+
]
|
| 3092 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d9/55/8a37bb9ce36541fd353466259a07ccfdfaf25c996f3a71d989af4d4c7ba4/utilsforecast-0.2.15.tar.gz", hash = "sha256:c36d65d698a88d0fadc93d2d6737c304c3776397c60ae551ee17aa678caf3659", size = 60609, upload-time = "2025-12-03T16:29:08.652Z" }
|
| 3093 |
+
wheels = [
|
| 3094 |
+
{ url = "https://files.pythonhosted.org/packages/6f/11/6c6ee61958b8e60f634b39e2f9a004f5d1c479cb962a2001fc3c72ceed78/utilsforecast-0.2.15-py3-none-any.whl", hash = "sha256:4b43bf5107e3cba13604cd86e93b5cf4906b57105b1900ccf98b8978aabd4150", size = 40344, upload-time = "2025-12-03T16:29:07.144Z" },
|
| 3095 |
+
]
|
| 3096 |
+
|
| 3097 |
[[package]]
|
| 3098 |
name = "uvicorn"
|
| 3099 |
version = "0.41.0"
|
|
|
|
| 3156 |
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
| 3157 |
]
|
| 3158 |
|
| 3159 |
+
[[package]]
|
| 3160 |
+
name = "wandb"
|
| 3161 |
+
version = "0.26.1"
|
| 3162 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3163 |
+
dependencies = [
|
| 3164 |
+
{ name = "click" },
|
| 3165 |
+
{ name = "gitpython" },
|
| 3166 |
+
{ name = "packaging" },
|
| 3167 |
+
{ name = "platformdirs" },
|
| 3168 |
+
{ name = "protobuf" },
|
| 3169 |
+
{ name = "pydantic" },
|
| 3170 |
+
{ name = "pyyaml" },
|
| 3171 |
+
{ name = "requests" },
|
| 3172 |
+
{ name = "sentry-sdk" },
|
| 3173 |
+
{ name = "typing-extensions" },
|
| 3174 |
+
]
|
| 3175 |
+
sdist = { url = "https://files.pythonhosted.org/packages/6a/a4/72a6640e1f566e81f184a426e3e45298d4c6672664de41adb7eb6f64370a/wandb-0.26.1.tar.gz", hash = "sha256:eef2dbaea06f0b1c0cdc5d76f544ae4c2b8848fc512442a00bd59f0502fc8aa1", size = 42159814, upload-time = "2026-04-23T16:27:34.033Z" }
|
| 3176 |
+
wheels = [
|
| 3177 |
+
{ url = "https://files.pythonhosted.org/packages/8c/09/3296235f3906e904f06f2df29eed4d672fb23c0932c9486e2af64f2f2a66/wandb-0.26.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:2955fe190c005fb83ee6d73f066c8a33f09f3212a1f2eb53faa6581440e456be", size = 24857204, upload-time = "2026-04-23T16:26:58.576Z" },
|
| 3178 |
+
{ url = "https://files.pythonhosted.org/packages/a1/ad/e39ca3086534129e42208ba00ed2c6247ce425f890219eeec33b4f162864/wandb-0.26.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:55d91cabde98162d7116a5e19ddd052bd9848556243f1da4cbb9ffb7ad435bfc", size = 26014649, upload-time = "2026-04-23T16:27:02.559Z" },
|
| 3179 |
+
{ url = "https://files.pythonhosted.org/packages/56/af/400d84a3bdce0b062b4baa70acb6becd2c8018697f4fbf5af9a9e1e406e5/wandb-0.26.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7c78bc2454cfe1ffa1c3a256060a387356eed8a4488e024d9d2eba8f2b5bd51d", size = 25421317, upload-time = "2026-04-23T16:27:06.411Z" },
|
| 3180 |
+
{ url = "https://files.pythonhosted.org/packages/7b/e9/b4bf8f3509dcea1cec52233a38991459654635b5a8e6a494eb912e1b9cfb/wandb-0.26.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:a2c8eeec8706dcd2872e69c3b4d20ec523082fdb4440295491556e219ad2aa67", size = 27192831, upload-time = "2026-04-23T16:27:10.308Z" },
|
| 3181 |
+
{ url = "https://files.pythonhosted.org/packages/62/cf/4a6dce0c782223ef0eeea7139daee73418a7322befcf083512c31cebaa18/wandb-0.26.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fa768ee0636a569afb7541cf996e56309c47070566a38916823f94e02afe586", size = 25593326, upload-time = "2026-04-23T16:27:14.259Z" },
|
| 3182 |
+
{ url = "https://files.pythonhosted.org/packages/df/99/58c3d8c36ae8e2b7d70bf6493eb5daa1cca0231a04b025717b4cd1a78f1e/wandb-0.26.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5854928725cfeff1f284d5c043cd353f810e5da02eead2c120ef5056ad026fea", size = 27535542, upload-time = "2026-04-23T16:27:18.473Z" },
|
| 3183 |
+
{ url = "https://files.pythonhosted.org/packages/7c/d0/4e846ffc1d0cc435518dfa581ce73ac82cfd0ebbf35f3853c9277f632e5f/wandb-0.26.1-py3-none-win32.whl", hash = "sha256:5c2bd44e575ae9944e2764d1aaa031461178276bf2636d5558399c2816ef5cfe", size = 24968151, upload-time = "2026-04-23T16:27:22.086Z" },
|
| 3184 |
+
{ url = "https://files.pythonhosted.org/packages/e3/9b/487413eaccefdb58799a226726e24b486e9192d2671c75a4550c160aba23/wandb-0.26.1-py3-none-win_amd64.whl", hash = "sha256:5817785467d3f1676f1812ec19a89f77f6e56dfe67d9f47080075af95f705d3e", size = 24968155, upload-time = "2026-04-23T16:27:25.731Z" },
|
| 3185 |
+
{ url = "https://files.pythonhosted.org/packages/04/dc/5baf3e99b3eeb709d6f75124b5bec8cb73d4b38d2b10df7fdcfde4966200/wandb-0.26.1-py3-none-win_arm64.whl", hash = "sha256:f848b7744f896bc04cabbb28360a2814d1551a91fa2c456243e06435729c8a2e", size = 22912416, upload-time = "2026-04-23T16:27:29.456Z" },
|
| 3186 |
+
]
|
| 3187 |
+
|
| 3188 |
[[package]]
|
| 3189 |
name = "wardrobe-os-ml-service"
|
| 3190 |
version = "0.0.1"
|
|
|
|
| 3212 |
{ name = "pyyaml" },
|
| 3213 |
{ name = "replicate" },
|
| 3214 |
{ name = "scikit-learn" },
|
| 3215 |
+
{ name = "timesfm" },
|
| 3216 |
{ name = "timm" },
|
| 3217 |
{ name = "torch" },
|
| 3218 |
{ name = "torchvision" },
|
|
|
|
| 3254 |
{ name = "pyyaml", specifier = ">=6.0.3" },
|
| 3255 |
{ name = "replicate", specifier = ">=1.0.0" },
|
| 3256 |
{ name = "scikit-learn", specifier = ">=1.4.0" },
|
| 3257 |
+
{ name = "timesfm", specifier = ">=1.3.0" },
|
| 3258 |
{ name = "timm", specifier = ">=1.0.25" },
|
| 3259 |
{ name = "torch", specifier = ">=2.5.0" },
|
| 3260 |
{ name = "torchvision", specifier = ">=0.20.0" },
|