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_ttm::config::TtmConfig;
10use zsfm_ttm::convert::{convert, ConvertOptions};
11
12#[derive(Subcommand)]
13pub enum Command {
14 Upload {
16 #[arg(short, long, default_value = "amaye15/ttm-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 = "ibm-granite/granite-timeseries-ttm-r2")]
33 model: String,
34 #[arg(short, long, default_value = "gguf/ttm-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 {
54 #[arg(short, long, default_value = "gguf/ttm-f32.gguf")]
56 gguf: PathBuf,
57 #[arg(
59 long,
60 default_value = "models/ibm-granite__granite-timeseries-ttm-r2/config.json"
61 )]
62 config: PathBuf,
63 },
64
65 Delete {
72 #[arg(short, long, default_value = "ibm-granite/granite-timeseries-ttm-r2")]
74 model: String,
75 #[arg(long, default_value = "models")]
77 model_dir: PathBuf,
78 #[arg(short, long)]
80 output: Option<PathBuf>,
81 },
82}
83
84pub async fn run(command: Command) -> anyhow::Result<()> {
85 match command {
86 Command::Delete {
87 model,
88 model_dir,
89 output,
90 } => {
91 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
92 crate::common::delete_cached_model(&canonical, output.as_deref())?;
93 }
94 Command::Upload { repo, token, root } => {
95 zsfm_hub::upload_repo(&repo, &token, &root).await?;
96 }
97
98 Command::Convert {
99 model,
100 output,
101 dtype,
102 model_dir,
103 token,
104 redownload,
105 } => {
106 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
107 if crate::common::try_recast_from_cache(&canonical, &output, dtype.into(), redownload)?
108 {
109 return Ok(());
110 }
111
112 eprintln!("Downloading {model} into {} …", model_dir.display());
113 let files = zsfm_hub::download_model(&model, token.as_deref(), &model_dir)
114 .await
115 .context("download failed")?;
116
117 let config_str =
118 std::fs::read_to_string(&files.config_json).context("read config.json")?;
119 let config = TtmConfig::from_json(&config_str).context("parse config.json")?;
120
121 println!(
122 "Config: context={}, pred={}, patch={}/{}, d_model={}, {} enc layers, {} dec layers, {} adaptive levels",
123 config.context_length, config.prediction_length,
124 config.patch_length, config.patch_stride,
125 config.d_model, config.num_layers, config.decoder_num_layers,
126 config.adaptive_patching_levels,
127 );
128
129 let f32_opts = ConvertOptions {
130 output_dtype: GGMLType::F32,
131 };
132 convert(&model, &files, &config, &f32_opts, &canonical)?;
133 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
134 files.cleanup_weights();
135
136 zsfm_checkpoint::recast(&canonical, &output, dtype.into())?;
137 eprintln!("Wrote {}", output.display());
138 }
139
140 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
141
142 Command::Infer { gguf, config } => {
143 return run_infer_burn(&gguf, &config).await;
144 }
145 }
146
147 Ok(())
148}
149
150async fn run_infer_burn(
151 gguf: &std::path::PathBuf,
152 config: &std::path::PathBuf,
153) -> anyhow::Result<()> {
154 use zsfm_ttm::infer::BurnTtmModel;
155
156 let buf = zsfm_core::read_stdin_limited()?;
157 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
158 let contexts = zsfm_core::parse_mv_contexts(req["context"].clone())?;
159 let horizon = zsfm_core::parse_horizon(&req)?;
160
161 let config_str =
162 std::fs::read_to_string(config).with_context(|| format!("read {}", config.display()))?;
163 let ttm_config = TtmConfig::from_json(&config_str).context("parse config.json")?;
164 let min_ctx = ttm_config.patch_length;
165
166 eprintln!("Loading Burn model from {} …", gguf.display());
167 let model = BurnTtmModel::load(gguf, ttm_config).context("load model")?;
168
169 let mut fc_outputs = Vec::new();
170 let mut total_ctx = 0usize;
171 for raw_variates in &contexts {
172 anyhow::ensure!(
173 raw_variates.len() == 1,
174 "TTM only supports univariate forecasting (1 variate per context)"
175 );
176 let ctx = &raw_variates[0];
177 anyhow::ensure!(
178 ctx.len() >= min_ctx,
179 "context too short — need at least {min_ctx} timesteps, got {}",
180 ctx.len()
181 );
182 total_ctx += ctx.len();
183 let raw = model.forecast(ctx).context("forecast")?;
184 let point: Vec<f32> = raw.into_iter().take(horizon).collect();
185 fc_outputs.push(ForecastOutput::Univariate {
186 point,
187 quantiles: Default::default(),
188 });
189 }
190 println!(
191 "{}",
192 zsfm_core::forecast_response_json("ttm", total_ctx, horizon, fc_outputs)?
193 );
194 Ok(())
195}