1use std::path::PathBuf;
2
3use crate::common::DtypeArg;
4use anyhow::Context;
5use clap::Subcommand;
6
7use zsfm_core::ForecastOutput;
8use zsfm_gguf::GGMLType;
9use zsfm_moment::config::MomentConfig;
10use zsfm_moment::convert::{convert, ConvertOptions};
11
12#[derive(Subcommand)]
13pub enum Command {
14 Upload {
16 #[arg(short, long, default_value = "amaye15/moment-gguf")]
17 repo: String,
18 #[arg(long, env = "HF_TOKEN")]
19 token: String,
20 #[arg(long, default_value = ".")]
22 root: PathBuf,
23 },
24 Convert {
32 #[arg(short, long, default_value = "AutonLab/MOMENT-1-large")]
33 model: String,
34 #[arg(short, long, default_value = "gguf/moment-f32.gguf")]
35 output: PathBuf,
36 #[arg(long, default_value = "f32")]
37 dtype: DtypeArg,
38 #[arg(long, default_value = "models")]
39 model_dir: PathBuf,
40 #[arg(long, env = "HF_TOKEN")]
41 token: Option<String>,
42 #[arg(long)]
44 redownload: bool,
45 },
46 InspectTensors { path: PathBuf },
49 Infer {
56 #[arg(short, long, default_value = "gguf/moment-f32.gguf")]
57 gguf: PathBuf,
58 },
59
60 Delete {
67 #[arg(short, long, default_value = "AutonLab/MOMENT-1-large")]
69 model: String,
70 #[arg(long, default_value = "models")]
72 model_dir: PathBuf,
73 #[arg(short, long)]
75 output: Option<PathBuf>,
76 },
77}
78
79pub async fn run(command: Command) -> anyhow::Result<()> {
80 match command {
81 Command::Delete {
82 model,
83 model_dir,
84 output,
85 } => {
86 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
87 crate::common::delete_cached_model(&canonical, output.as_deref())?;
88 }
89 Command::Upload { repo, token, root } => {
90 zsfm_hub::upload_repo(&repo, &token, &root).await?;
91 }
92
93 Command::Convert {
94 model,
95 output,
96 dtype,
97 model_dir,
98 token,
99 redownload,
100 } => {
101 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
102 if crate::common::try_recast_from_cache(&canonical, &output, dtype.into(), redownload)?
103 {
104 return Ok(());
105 }
106
107 eprintln!("Downloading {model} into {} …", model_dir.display());
108 let files = zsfm_hub::download_model(&model, token.as_deref(), &model_dir)
109 .await
110 .context("download failed")?;
111
112 let config = MomentConfig::default();
113 let f32_opts = ConvertOptions {
114 output_dtype: GGMLType::F32,
115 };
116 convert(&files.safetensors_shards, &config, &f32_opts, &canonical)?;
117 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
118 files.cleanup_weights();
119
120 zsfm_checkpoint::recast(&canonical, &output, dtype.into())?;
121 eprintln!("Wrote {}", output.display());
122 }
123
124 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
125
126 Command::Infer { gguf } => {
127 return run_infer_burn(&gguf).await;
128 }
129 }
130
131 Ok(())
132}
133
134async fn run_infer_burn(gguf: &std::path::PathBuf) -> anyhow::Result<()> {
135 use zsfm_moment::infer::BurnMomentModel;
136
137 let buf = zsfm_core::read_stdin_limited()?;
138 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
139 let contexts = zsfm_core::parse_mv_contexts(req["context"].clone())?;
140 let horizon = zsfm_core::parse_horizon(&req)?;
141 let config = MomentConfig::default();
142
143 eprintln!("Loading Burn model from {} …", gguf.display());
144 let model = BurnMomentModel::load(gguf, config).context("load model")?;
145
146 let mut fc_outputs = Vec::new();
147 let mut total_ctx = 0usize;
148 for raw_variates in &contexts {
149 anyhow::ensure!(
150 raw_variates.len() == 1,
151 "MOMENT only supports univariate forecasting (1 variate per context)"
152 );
153 let ctx = &raw_variates[0];
154 anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
155 total_ctx += ctx.len();
156 let point = model.forecast(ctx, horizon).context("forecast")?;
157 fc_outputs.push(zsfm_core::ForecastOutput::Univariate {
158 point,
159 quantiles: Default::default(),
160 });
161 }
162 println!(
163 "{}",
164 zsfm_core::forecast_response_json("moment", total_ctx, horizon, fc_outputs)?
165 );
166 Ok(())
167}