xiezhe22 commited on
Commit
5cf30c0
Β·
1 Parent(s): 15887ea
Files changed (2) hide show
  1. app.py +124 -256
  2. requirements.txt +1 -1
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import spaces # for ZeroGPU support
2
  import gradio as gr
3
  import pandas as pd
@@ -11,7 +12,7 @@ from transformers import (
11
  TextIteratorStreamer
12
  )
13
 
14
- # MODEL SETUP
15
  MODEL_NAME = "bytedance-research/ChatTS-14B"
16
 
17
  tokenizer = AutoTokenizer.from_pretrained(
@@ -28,37 +29,70 @@ model = AutoModelForCausalLM.from_pretrained(
28
  )
29
  model.eval()
30
 
31
- # ─── HELPER FUNCTIONS ──────────────────────────────────────────────────────────
32
- def create_default_timeseries():
33
- """Create default time series with a sudden change.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- Returns a pandas DataFrame containing two simple synthetic time series.
36
- """
37
- x1 = np.arange(256)
38
- x2 = np.arange(256)
39
- ts1 = np.sin(x1 / 10) * 5.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  ts1[103:] -= 10.0
41
- ts2 = x2 * 0.01
42
  ts2[100] += 10.0
43
- df = pd.DataFrame({
44
- "TS1": ts1,
45
- "TS2": ts2
46
- })
47
- return df
48
 
49
  def process_csv_file(csv_file):
50
- """Process CSV file and return DataFrame with validation.
51
-
52
- Validates the uploaded CSV file and ensures that each column represents a
53
- time series. Returns (df, message) where df is a pandas DataFrame or None
54
- and message is a user friendly status string.
55
- """
56
  if csv_file is None:
57
  return None, "No file uploaded"
58
  try:
59
- # pandas will handle file objects, but for gradio we want to read by name
60
  df = pd.read_csv(csv_file.name)
61
- # strip and drop empty column names
62
  df.columns = [str(c).strip() for c in df.columns]
63
  df = df.loc[:, [c for c in df.columns if c]]
64
  df = df.dropna(axis=1, how="all")
@@ -66,117 +100,66 @@ def process_csv_file(csv_file):
66
  return None, "No valid time-series columns found."
67
  if df.shape[1] > 15:
68
  return None, f"Too many series ({df.shape[1]}). Max allowed = 15."
69
- # Validate each column
70
- ts_names, ts_list = [], []
71
  for name in df.columns:
72
  series = df[name]
73
- # ensure numeric
74
  if not pd.api.types.is_float_dtype(series):
75
- try:
76
- series = pd.to_numeric(series, errors='coerce')
77
- except Exception:
78
- return None, f"Series '{name}' cannot be converted to float type."
79
  last_valid = series.last_valid_index()
80
  if last_valid is None:
81
  continue
82
  trimmed = series.loc[:last_valid].to_numpy(dtype=np.float32)
83
- length = len(trimmed)
84
- if length < 64 or length > 1024:
85
- return None, f"Series '{name}' length {length} invalid. Must be 64 to 1024."
86
- ts_names.append(name)
87
- ts_list.append(trimmed)
88
  if not ts_list:
89
  return None, "All time series are empty after trimming NaNs."
90
- return df, f"Successfully loaded {len(ts_names)} time series: {', '.join(ts_names)}"
91
  except Exception as e:
92
  return None, f"Error processing file: {str(e)}"
93
 
94
  def preview_csv(csv_file, use_default):
95
- """Preview uploaded CSV file or default time series immediately.
96
-
97
- Returns a gr.LinePlot, status message, dropdown component, and a boolean
98
- indicating whether the default dataset is being used.
99
- """
100
  if csv_file is None:
101
- # no file, return empty plot and message
102
  return gr.LinePlot(value=pd.DataFrame()), "Please upload a CSV file first", gr.Dropdown(), False
103
  df, message = process_csv_file(csv_file)
104
  if df is None:
105
  return gr.LinePlot(value=pd.DataFrame()), message, gr.Dropdown(), False
106
- # Build dropdown choices and default plot
107
- column_choices = list(df.columns)
108
- first_column = column_choices[0]
109
- df_with_index = df.copy()
110
- df_with_index["_internal_idx"] = np.arange(len(df[first_column].values))
111
- plot = gr.LinePlot(
112
- df_with_index,
113
- x="_internal_idx",
114
- y=first_column,
115
- title=f"Time Series: {first_column}"
116
- )
117
- dropdown = gr.Dropdown(
118
- choices=column_choices,
119
- value=first_column,
120
- label="Select a Column to Visualize"
121
- )
122
  return plot, message, dropdown, False
123
 
124
  def clear_csv():
125
- """Clear uploaded CSV file immediately."""
126
- return gr.LinePlot(value=pd.DataFrame()), "No file uploaded", gr.Dropdown()
127
 
128
  def update_plot(csv_file, selected_column, use_default_state):
129
- """Update plot based on selected column or default state."""
130
  if (csv_file is None and not use_default_state) or selected_column is None:
131
  return gr.LinePlot(value=pd.DataFrame())
132
  if csv_file is None and use_default_state:
133
  df = create_default_timeseries()
134
  else:
135
  df, _ = process_csv_file(csv_file)
136
- if df is None:
137
- return gr.LinePlot(value=pd.DataFrame())
138
- df_with_index = df.copy()
139
- df_with_index["_internal_idx"] = np.arange(len(df[selected_column].values))
140
- plot = gr.LinePlot(
141
- df_with_index,
142
- x="_internal_idx",
143
- y=selected_column,
144
- title=f"Time Series: {selected_column}"
145
- )
146
- return plot
147
 
148
  def initialize_interface():
149
- """Initialize interface with default time series.
150
-
151
- Returns a plot, a status message, dropdown and boolean state.
152
- """
153
  df = create_default_timeseries()
154
- column_choices = list(df.columns)
155
- first_column = column_choices[0]
156
- df_with_index = df.copy()
157
- df_with_index["_internal_idx"] = np.arange(len(df[first_column].values))
158
- plot = gr.LinePlot(
159
- df_with_index,
160
- x="_internal_idx",
161
- y=first_column,
162
- title=f"Time Series: {first_column}"
163
- )
164
- dropdown = gr.Dropdown(
165
- choices=column_choices,
166
- value=first_column,
167
- label="Select a Column to Visualize"
168
- )
169
- message = "Using default time series (TS1 and TS2). Select a series from the dropdown for visualization."
170
- return plot, message, dropdown, True
171
 
 
172
  @spaces.GPU
173
  def infer_chatts_stream(prompt: str, csv_file, use_default):
174
- """
175
- Streaming version of ChatTS inference.
176
-
177
- Constructs a full prompt using the uploaded or default time series and yields
178
- the assistant's response incrementally.
179
- """
180
  if not prompt.strip():
181
  yield "Please enter a prompt"
182
  return
@@ -189,15 +172,12 @@ def infer_chatts_stream(prompt: str, csv_file, use_default):
189
  yield "Please upload a CSV file first or the file contains errors"
190
  return
191
  try:
192
- # Prepare time series data
193
  ts_names, ts_list = [], []
194
  for name in df.columns:
195
- series = df[name]
196
- last_valid = series.last_valid_index()
197
  if last_valid is not None:
198
  trimmed = series.loc[:last_valid].to_numpy(dtype=np.float32)
199
- ts_names.append(name)
200
- ts_list.append(trimmed)
201
  if not ts_list:
202
  yield "No valid time series data found. Please upload time series first."
203
  return
@@ -207,186 +187,74 @@ def infer_chatts_stream(prompt: str, csv_file, use_default):
207
  prefix += f"The {name} is of length {len(arr)}: <ts><ts/>\n"
208
  full_prompt = (
209
  "<|im_start|>system\nYou are a helpful assistant. Your name is ChatTS. "
210
- "You can analyze time series data and provide insights. If the user asks who you are, "
211
- "you should give your name and capabilities in the language of the prompt. "
212
- "If no time series are provided, you should say 'I cannot answer this question as you "
213
- "haven't provided the timeseries I need' in the language of the prompt. "
214
- "Always check if the user has provided at least one time series data before answering."
215
  "<|im_end|><|im_start|>user\n"
216
- f"{prefix}{clean_prompt} "
217
- "Please output a step-by-step analysis about the time series attributes that are mentioned "
218
- "in the question first, and then give a detailed result about this question. "
219
- "Always remember to carefully double check the values before answering the results."
220
  "<|im_end|><|im_start|>assistant\n"
221
  )
222
- inputs = processor(
223
- text=[full_prompt],
224
- timeseries=ts_list,
225
- padding=True,
226
- return_tensors="pt"
227
- )
228
  inputs = {k: v.to(model.device) for k, v in inputs.items()}
229
  streamer = TextIteratorStreamer(tokenizer, timeout=10., skip_prompt=True, skip_special_tokens=True)
230
- inputs.update({
231
- "max_new_tokens": 512,
232
- "streamer": streamer,
233
- "temperature": 0.3
234
- })
235
- thread = Thread(
236
- target=model.generate,
237
- kwargs=inputs
238
- )
239
- thread.start()
240
- model_output = ""
241
  for new_text in streamer:
242
- model_output += new_text
243
- yield model_output
244
  except Exception as e:
245
  yield f"Error during inference: {str(e)}"
246
 
247
- # Example prompts for quick analysis.
248
- EXAMPLE_PROMPTS = [
249
- "Analyze all the given time series and summarize the overall trends.",
250
- "Identify and describe any anomalies in each time series.",
251
- "Compare the patterns between the first and second time series.",
252
- "Describe any seasonal patterns present in the time series.",
253
- "Find the maximum and minimum values in each series and comment on them."
254
- ]
255
-
256
- # Custom CSS to style the app in a dark theme and create card-like example buttons.
257
- CSS = """
258
- /* Dark background and base text color */
259
- .gradio-container, .gr-block, .gr-row, .gr-column {
260
- background-color: #111827 !important;
261
- color: #f3f4f6 !important;
262
- }
263
- /* Style labels */
264
- label, .gr-textbox label, .gr-dropdown label {
265
- color: #f3f4f6 !important;
266
- }
267
- /* Style input elements */
268
- textarea, select, input[type=text], input[type=number], .gr-dropdown select {
269
- background-color: #1f2937 !important;
270
- color: #f3f4f6 !important;
271
- border: 1px solid #374151 !important;
272
- }
273
- /* Buttons styling */
274
- button, .gr-button {
275
- background-color: #374151 !important;
276
- color: #f3f4f6 !important;
277
- border: 1px solid #4b5563 !important;
278
- }
279
- button:hover, .gr-button:hover {
280
- background-color: #4b5563 !important;
281
- }
282
- /* Example card buttons */
283
- .example-card {
284
- background-color: #1f2937 !important;
285
- color: #f3f4f6 !important;
286
- border: 1px solid #4b5563 !important;
287
- border-radius: 8px;
288
- padding: 10px;
289
- margin-right: 10px;
290
- cursor: pointer;
291
- display: flex;
292
- justify-content: center;
293
- align-items: center;
294
- transition: background-color 0.2s ease;
295
  }
296
- .example-card:hover {
297
- background-color: #374151 !important;
298
- }
299
- """
300
 
301
- # ─── BUILD THE GRADIO APP ─────────────────────────────────────────────────────
 
 
302
 
303
- with gr.Blocks(title="ChatTS Demo (Dark Theme)", css=CSS) as demo:
304
- # Create example buttons in a row.
305
- with gr.Row():
306
- example_buttons = []
307
- for idx, prompt_text in enumerate(EXAMPLE_PROMPTS):
308
- btn = gr.Button(f"Example {idx+1}", elem_classes="example-card")
309
- example_buttons.append((btn, prompt_text))
310
 
311
- # Hidden state to track whether to use default time series
312
- use_default_state = gr.State(value=True)
 
 
 
313
 
314
- # Upload, prompt input and run button
315
  with gr.Row():
316
  with gr.Column(scale=1):
317
- upload = gr.File(
318
- label="Upload CSV File",
319
- file_types=[".csv"],
320
- type="filepath"
321
- )
322
  prompt_input = gr.Textbox(
323
  lines=6,
324
  placeholder="Enter your question here...",
325
  label="Analysis Prompt",
326
- value="Please analyze all the given time series and provide insights about the local fluctuations in the time series in detail."
327
  )
328
  run_btn = gr.Button("Run ChatTS", variant="primary")
329
  with gr.Column(scale=2):
330
- series_selector = gr.Dropdown(
331
- label="Select a Column to Visualize",
332
- choices=[],
333
- value=None
334
- )
335
  plot_out = gr.LinePlot(value=pd.DataFrame(), label="Time Series Visualization")
336
- file_status = gr.Textbox(
337
- label="File Status",
338
- interactive=False,
339
- lines=2
340
- )
341
 
342
- # Output area
343
- text_out = gr.Textbox(
344
- lines=10,
345
- label="ChatTS Analysis Results",
346
- interactive=False
347
- )
348
 
349
- # Initialize with default data
350
- demo.load(
351
- fn=initialize_interface,
352
- outputs=[plot_out, file_status, series_selector, use_default_state]
353
- )
354
 
355
- # Event handlers for upload and dropdown
356
- upload.upload(
357
- fn=preview_csv,
358
- inputs=[upload, use_default_state],
359
- outputs=[plot_out, file_status, series_selector, use_default_state]
360
- )
361
- upload.clear(
362
- fn=clear_csv,
363
- inputs=[],
364
- outputs=[plot_out, file_status, series_selector]
365
- )
366
- series_selector.change(
367
- fn=update_plot,
368
- inputs=[upload, series_selector, use_default_state],
369
- outputs=[plot_out]
370
- )
371
 
372
- # Event handler for run button to perform inference
373
- run_btn.click(
374
- fn=infer_chatts_stream,
375
- inputs=[prompt_input, upload, use_default_state],
376
- outputs=[text_out]
377
- )
378
-
379
- # Example buttons update the prompt input when clicked
380
- for btn, prompt_text in example_buttons:
381
- # Use closure to capture prompt_text
382
- def click_fn(p=prompt_text):
383
- return p
384
- btn.click(
385
- fn=click_fn,
386
- inputs=[],
387
- outputs=[prompt_input],
388
- queue=False # no queuing required for simple update
389
- )
390
 
391
  if __name__ == '__main__':
392
  demo.launch()
 
1
+
2
  import spaces # for ZeroGPU support
3
  import gradio as gr
4
  import pandas as pd
 
12
  TextIteratorStreamer
13
  )
14
 
15
+ # ─── MODEL SETUP ────────────────────────────────────────────────────────────────
16
  MODEL_NAME = "bytedance-research/ChatTS-14B"
17
 
18
  tokenizer = AutoTokenizer.from_pretrained(
 
29
  )
30
  model.eval()
31
 
32
+ # ─── VISUAL THEME (Dark) ───────────────────────────────────────────────────────
33
+ CSS = """
34
+ :root{
35
+ --bg:#0b1020; --bg-2:#10172e; --panel:#141b34; --panel-2:#0f172a;
36
+ --border:#263154; --fg:#e7ecf5; --muted:#b9c2d3; --placeholder:#7c8195;
37
+ }
38
+ /* App background */
39
+ .gradio-container{background: linear-gradient(180deg,var(--bg),var(--bg) 40%, var(--bg-2)); color: var(--fg);}
40
+ /* Generic block panels */
41
+ .gradio-container .block, .gradio-container .form, .gradio-container .padded{
42
+ background: linear-gradient(180deg,var(--panel),var(--panel-2));
43
+ border: 1px solid var(--border);
44
+ border-radius: 14px;
45
+ }
46
+ /* Inputs */
47
+ input[type="text"], textarea, select, .gr-textbox, .gr-dropdown, .gr-textbox textarea{
48
+ background: #0f1630 !important; color: var(--fg) !important;
49
+ border: 1px solid var(--border) !important; border-radius: 12px !important;
50
+ }
51
+ .gr-textbox textarea::placeholder{ color: var(--placeholder) !important; }
52
+ label, .svelte-1gfkn6j, .svelte-i3tvor, .label{ color: var(--muted) !important; }
53
 
54
+ /* Upload panel */
55
+ button.svelte-1x5qevo{ background:#0f1630 !important; color:var(--muted) !important; border-color:var(--border) !important; }
56
+ .svelte-12ioyct{ color: var(--muted) !important; }
57
+
58
+ /* Line plot container */
59
+ .vega-embed, canvas.marks{ background:#0b1020 !important; border-radius: 10px; }
60
+
61
+ /* Buttons base */
62
+ .gr-button{ border-radius:14px !important; border:1px solid var(--border) !important; color:#0b1020 !important; font-weight:700; }
63
+ .gr-button:hover{ transform: translateY(-1px); box-shadow:0 6px 18px rgba(0,0,0,.35); }
64
+ .gr-button-primary{ background: linear-gradient(180deg,#8ab4ff,#7aa2ff) !important; }
65
+ .gr-button-secondary{ background: linear-gradient(180deg,#a78bfa,#7aa2ff) !important; color:#0b1020 !important; }
66
+ .btn-chip{ padding:12px 16px !important; }
67
+
68
+ /* Example cards row */
69
+ .examples-row{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px; margin:12px 0 8px; }
70
+ .cardbtn{ border:1px solid var(--border) !important; color:#0b1020 !important; }
71
+ .grad-blue{ background: linear-gradient(180deg,#93c5fd,#60a5fa) !important; }
72
+ .grad-purple{ background: linear-gradient(180deg,#c4b5fd,#a78bfa) !important; }
73
+ .grad-green{ background: linear-gradient(180deg,#bbf7d0,#34d399) !important; }
74
+ .grad-amber{ background: linear-gradient(180deg,#fde68a,#f59e0b) !important; }
75
+ .grad-rose{ background: linear-gradient(180deg,#fecaca,#fb7185) !important; }
76
+ .cardbtn:hover{ filter:brightness(1.02); transform:translateY(-1px); }
77
+
78
+ /* Subtle spacing fixes */
79
+ .gap, .padded{ padding:12px !important; }
80
+ """
81
+
82
+ # ─── HELPERS ───────────────────────────────────────────────────────────────────
83
+ def create_default_timeseries():
84
+ x = np.arange(256)
85
+ ts1 = np.sin(x / 10) * 5.0
86
  ts1[103:] -= 10.0
87
+ ts2 = x * 0.01
88
  ts2[100] += 10.0
89
+ return pd.DataFrame({"TS1": ts1, "TS2": ts2})
 
 
 
 
90
 
91
  def process_csv_file(csv_file):
 
 
 
 
 
 
92
  if csv_file is None:
93
  return None, "No file uploaded"
94
  try:
 
95
  df = pd.read_csv(csv_file.name)
 
96
  df.columns = [str(c).strip() for c in df.columns]
97
  df = df.loc[:, [c for c in df.columns if c]]
98
  df = df.dropna(axis=1, how="all")
 
100
  return None, "No valid time-series columns found."
101
  if df.shape[1] > 15:
102
  return None, f"Too many series ({df.shape[1]}). Max allowed = 15."
103
+ ts_names = []
104
+ ts_list = []
105
  for name in df.columns:
106
  series = df[name]
 
107
  if not pd.api.types.is_float_dtype(series):
108
+ series = pd.to_numeric(series, errors='coerce')
 
 
 
109
  last_valid = series.last_valid_index()
110
  if last_valid is None:
111
  continue
112
  trimmed = series.loc[:last_valid].to_numpy(dtype=np.float32)
113
+ L = trimmed.shape[0]
114
+ if L < 64 or L > 1024:
115
+ return None, f"Series '{name}' length {L} invalid. Must be 64 to 1024."
116
+ ts_names.append(name); ts_list.append(trimmed)
 
117
  if not ts_list:
118
  return None, "All time series are empty after trimming NaNs."
119
+ return df, f"Loaded {len(ts_names)} series: {', '.join(ts_names)}"
120
  except Exception as e:
121
  return None, f"Error processing file: {str(e)}"
122
 
123
  def preview_csv(csv_file, use_default):
 
 
 
 
 
124
  if csv_file is None:
 
125
  return gr.LinePlot(value=pd.DataFrame()), "Please upload a CSV file first", gr.Dropdown(), False
126
  df, message = process_csv_file(csv_file)
127
  if df is None:
128
  return gr.LinePlot(value=pd.DataFrame()), message, gr.Dropdown(), False
129
+ choices = list(df.columns)
130
+ first = choices[0]
131
+ df_idx = df.copy(); df_idx["_i"] = np.arange(len(df[first].values))
132
+ plot = gr.LinePlot(df_idx, x="_i", y=first, title=f"Time Series: {first}")
133
+ dropdown = gr.Dropdown(choices=choices, value=first, label="Select a Column to Visualize")
 
 
 
 
 
 
 
 
 
 
 
134
  return plot, message, dropdown, False
135
 
136
  def clear_csv():
137
+ return gr.LinePlot(value=pd.DataFrame()), "Cleared.", gr.Dropdown()
 
138
 
139
  def update_plot(csv_file, selected_column, use_default_state):
 
140
  if (csv_file is None and not use_default_state) or selected_column is None:
141
  return gr.LinePlot(value=pd.DataFrame())
142
  if csv_file is None and use_default_state:
143
  df = create_default_timeseries()
144
  else:
145
  df, _ = process_csv_file(csv_file)
146
+ if df is None:
147
+ return gr.LinePlot(value=pd.DataFrame())
148
+ df_idx = df.copy(); df_idx["_i"] = np.arange(len(df[selected_column].values))
149
+ return gr.LinePlot(df_idx, x="_i", y=selected_column, title=f"Time Series: {selected_column}")
 
 
 
 
 
 
 
150
 
151
  def initialize_interface():
 
 
 
 
152
  df = create_default_timeseries()
153
+ choices = list(df.columns); first = choices[0]
154
+ df_idx = df.copy(); df_idx["_i"] = np.arange(len(df[first].values))
155
+ plot = gr.LinePlot(df_idx, x="_i", y=first, title=f"Time Series: {first}")
156
+ dropdown = gr.Dropdown(choices=choices, value=first, label="Select a Column to Visualize")
157
+ msg = "Using default time series (TS1 and TS2). Select a series from the dropdown for visualization."
158
+ return plot, msg, dropdown, True
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # ─── INFERENCE ─────────────────────────────────────────────────────────────────
161
  @spaces.GPU
162
  def infer_chatts_stream(prompt: str, csv_file, use_default):
 
 
 
 
 
 
163
  if not prompt.strip():
164
  yield "Please enter a prompt"
165
  return
 
172
  yield "Please upload a CSV file first or the file contains errors"
173
  return
174
  try:
 
175
  ts_names, ts_list = [], []
176
  for name in df.columns:
177
+ series = df[name]; last_valid = series.last_valid_index()
 
178
  if last_valid is not None:
179
  trimmed = series.loc[:last_valid].to_numpy(dtype=np.float32)
180
+ ts_names.append(name); ts_list.append(trimmed)
 
181
  if not ts_list:
182
  yield "No valid time series data found. Please upload time series first."
183
  return
 
187
  prefix += f"The {name} is of length {len(arr)}: <ts><ts/>\n"
188
  full_prompt = (
189
  "<|im_start|>system\nYou are a helpful assistant. Your name is ChatTS. "
190
+ "You can analyze time series data and provide insights. If user asks who you are, you should give your name and capabilities "
191
+ "in the language of the prompt. If no time series are provided, you should say 'I cannot answer this question as you haven't provide the timeseries I need' "
192
+ "in the language of the prompt. Always check if the user has provided at least one time series data before answering."
 
 
193
  "<|im_end|><|im_start|>user\n"
194
+ f"{prefix}{clean_prompt} Please output a step-by-step analysis about the time series attributes that mentioned in the question first, "
195
+ "and then give a detailed result about this question. Always remember to carefully double check the values before answer the results."
 
 
196
  "<|im_end|><|im_start|>assistant\n"
197
  )
198
+ inputs = processor(text=[full_prompt], timeseries=ts_list, padding=True, return_tensors="pt")
 
 
 
 
 
199
  inputs = {k: v.to(model.device) for k, v in inputs.items()}
200
  streamer = TextIteratorStreamer(tokenizer, timeout=10., skip_prompt=True, skip_special_tokens=True)
201
+ inputs.update({"max_new_tokens": 512, "streamer": streamer, "temperature": 0.3})
202
+ thread = Thread(target=model.generate, kwargs=inputs); thread.start()
203
+ out = ""
 
 
 
 
 
 
 
 
204
  for new_text in streamer:
205
+ out += new_text
206
+ yield out
207
  except Exception as e:
208
  yield f"Error during inference: {str(e)}"
209
 
210
+ # ─── EXAMPLES (semantic names) ─────────────────────────────────────────────────
211
+ EXAMPLE_PROMPTS = {
212
+ "πŸ” Detect Spikes": "Identify all spikes in each series <ts><ts/> and report timestamps and magnitudes. Explain briefly.",
213
+ "πŸ“ˆ Trend & Seasonality": "Describe the trend and seasonality for the provided time series <ts><ts/>. Estimate the dominant period length and amplitude.",
214
+ "πŸ”— Compare Metrics": "Compare the two series <ts><ts/>. Are there lagged correlations? Estimate the lag and correlation strength.",
215
+ "⚑ Local Change Analysis": "Find intervals with >10% rise or drop relative to the prior 20 points for <ts><ts/>. Return intervals and reasons.",
216
+ "πŸ“Š Correlation Strength": "Quantify Pearson correlation between each pair of series <ts><ts/> and highlight the strongest positive/negative pairs."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  }
 
 
 
 
218
 
219
+ # ─── UI ────────────────────────────────────────────────────────────────────────
220
+ with gr.Blocks(title="ChatTS Demo", css=CSS) as demo:
221
+ use_default_state = gr.State(value=True)
222
 
223
+ gr.HTML("<h3 style='text-align:center; color:#e8ebfa; font-size:20px; font-weight:700; margin-top:8px;'>πŸ’‘ Click an example below and click Run ChatTS to try different questions with default time series or upload your own CSV file.</h3>")
 
 
 
 
 
 
224
 
225
+ with gr.Row(elem_classes="examples-row"):
226
+ btns = {}
227
+ grads = ["grad-blue","grad-purple","grad-green","grad-amber","grad-rose"]
228
+ for (title, prompt), grad in zip(EXAMPLE_PROMPTS.items(), grads):
229
+ btns[title] = gr.Button(title, elem_classes=f"cardbtn btn-chip {grad}")
230
 
 
231
  with gr.Row():
232
  with gr.Column(scale=1):
233
+ upload = gr.File(label="", file_types=[".csv"], type="filepath")
 
 
 
 
234
  prompt_input = gr.Textbox(
235
  lines=6,
236
  placeholder="Enter your question here...",
237
  label="Analysis Prompt",
238
+ value="Find the maximum and minimum values in each series and comment on them."
239
  )
240
  run_btn = gr.Button("Run ChatTS", variant="primary")
241
  with gr.Column(scale=2):
242
+ series_selector = gr.Dropdown(label="Select a Column to Visualize", choices=[], value=None)
 
 
 
 
243
  plot_out = gr.LinePlot(value=pd.DataFrame(), label="Time Series Visualization")
244
+ file_status = gr.Textbox(label="File Status", interactive=False, lines=2)
 
 
 
 
245
 
246
+ text_out = gr.Textbox(lines=10, label="ChatTS Analysis Results", interactive=False)
 
 
 
 
 
247
 
248
+ demo.load(fn=initialize_interface, outputs=[plot_out, file_status, series_selector, use_default_state])
 
 
 
 
249
 
250
+ for title, prompt in EXAMPLE_PROMPTS.items():
251
+ btns[title].click(fn=lambda p=prompt: p, outputs=prompt_input)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ upload.upload(fn=preview_csv, inputs=[upload, use_default_state],
254
+ outputs=[plot_out, file_status, series_selector, use_default_state])
255
+ upload.clear(fn=clear_csv, outputs=[plot_out, file_status, series_selector])
256
+ series_selector.change(fn=update_plot, inputs=[upload, series_selector, use_default_state], outputs=[plot_out])
257
+ run_btn.click(fn=infer_chatts_stream, inputs=[prompt_input, upload, use_default_state], outputs=[text_out])
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  if __name__ == '__main__':
260
  demo.launch()
requirements.txt CHANGED
@@ -4,4 +4,4 @@ numpy
4
  pandas
5
  accelerate
6
  torch>=2.2.0
7
- pydantic==2.10.6
 
4
  pandas
5
  accelerate
6
  torch>=2.2.0
7
+ pydantic==2.10.6