1use std::sync::Arc;
2
3use base64::Engine as _;
4use mold_core::{ImageData, OutputFormat, SseCompleteEvent, SseErrorEvent, SseProgressEvent};
5use sha2::{Digest, Sha256};
6use std::time::{Instant, SystemTime, UNIX_EPOCH};
7
8use crate::model_manager;
9use crate::state::{
10 ActiveGenerationSnapshot, AppState, GenerationJob, GenerationJobResult, SseMessage,
11};
12
13fn progress_to_sse(event: mold_inference::ProgressEvent) -> SseProgressEvent {
15 event.into()
16}
17
18pub(crate) fn clean_error_message(e: &anyhow::Error) -> String {
20 let full = format!("{e}");
21 let mut lines: Vec<&str> = Vec::new();
22 for line in full.lines() {
23 let trimmed = line.trim_start();
24 if (trimmed.starts_with("0:") || trimmed.starts_with("1:"))
25 && trimmed.len() > 3
26 && trimmed
27 .as_bytes()
28 .first()
29 .is_some_and(|b| b.is_ascii_digit())
30 {
31 break;
32 }
33 if trimmed.len() > 2
34 && trimmed.as_bytes()[0].is_ascii_digit()
35 && trimmed.contains("::")
36 && trimmed.contains("at ")
37 {
38 break;
39 }
40 lines.push(line);
41 }
42 let msg = lines.join("\n").trim().to_string();
43 if msg.is_empty() {
44 format!("{}", e.root_cause())
45 } else {
46 msg
47 }
48}
49
50fn set_active_generation(state: &AppState, model: &str, prompt: &str) {
51 let prompt_sha256 = format!("{:x}", Sha256::digest(prompt.as_bytes()));
52 let started_at_unix_ms = SystemTime::now()
53 .duration_since(UNIX_EPOCH)
54 .unwrap_or_default()
55 .as_millis() as u64;
56
57 let mut active = state
58 .active_generation
59 .write()
60 .unwrap_or_else(|e| e.into_inner());
61 *active = Some(ActiveGenerationSnapshot {
62 model: model.to_string(),
63 prompt_sha256,
64 started_at_unix_ms,
65 started_at: Instant::now(),
66 });
67}
68
69fn clear_active_generation(state: &AppState) {
70 let mut active = state
71 .active_generation
72 .write()
73 .unwrap_or_else(|e| e.into_inner());
74 *active = None;
75}
76
77fn save_image_to_dir(
78 dir: &std::path::Path,
79 img: &mold_core::ImageData,
80 model: &str,
81 batch_size: u32,
82) {
83 if let Err(e) = std::fs::create_dir_all(dir) {
84 tracing::warn!("failed to create output dir {}: {e}", dir.display());
85 return;
86 }
87 let timestamp_ms = SystemTime::now()
88 .duration_since(UNIX_EPOCH)
89 .unwrap_or_default()
90 .as_millis() as u64;
91 let ext = img.format.to_string();
92 let filename =
93 mold_core::default_output_filename(model, timestamp_ms, &ext, batch_size, img.index);
94 let path = dir.join(&filename);
95 match std::fs::write(&path, &img.data) {
96 Ok(()) => tracing::info!("saved image to {}", path.display()),
97 Err(e) => tracing::warn!("failed to save image to {}: {e}", path.display()),
98 }
99}
100
101pub async fn run_queue_worker(
104 mut job_rx: tokio::sync::mpsc::Receiver<GenerationJob>,
105 state: AppState,
106) {
107 tracing::debug!("generation queue worker started");
108 while let Some(job) = job_rx.recv().await {
109 #[cfg(feature = "metrics")]
110 crate::metrics::record_queue_depth(state.queue.pending());
111 process_job(&state, job).await;
112 state.queue.decrement();
113 #[cfg(feature = "metrics")]
114 crate::metrics::record_queue_depth(state.queue.pending());
115 }
116 tracing::info!("generation queue worker shutting down");
117}
118
119async fn process_job(state: &AppState, job: GenerationJob) {
120 if job.result_tx.is_closed() {
122 tracing::debug!("skipping queued job — client disconnected");
123 return;
124 }
125
126 if let Some(ref tx) = job.progress_tx {
128 let _ = tx.send(SseMessage::Progress(SseProgressEvent::Queued {
129 position: 0,
130 }));
131 }
132
133 let progress_callback = job.progress_tx.as_ref().map(|tx| {
135 let tx = tx.clone();
136 Arc::new(move |event: mold_inference::ProgressEvent| {
137 let _ = tx.send(SseMessage::Progress(progress_to_sse(event)));
138 }) as model_manager::EngineProgressCallback
139 });
140
141 if let Err(api_err) =
142 model_manager::ensure_model_ready(state, &job.request.model, progress_callback).await
143 {
144 let err_msg = api_err.error.clone();
145 if let Some(ref tx) = job.progress_tx {
146 let _ = tx.send(SseMessage::Error(SseErrorEvent {
147 message: err_msg.clone(),
148 }));
149 }
150 let _ = job.result_tx.send(Err(err_msg));
151 return;
152 }
153
154 #[cfg(target_os = "macos")]
156 if let Some(available) = mold_inference::device::available_system_memory_bytes() {
157 if available < 1_000_000_000 {
158 tracing::warn!(
159 available_mb = available / 1_000_000,
160 "low memory before inference — system may become unstable"
161 );
162 }
163 }
164
165 let model_cache = state.model_cache.clone();
167 let active_gen = state.active_generation.clone();
168 let gen_state = state.clone();
169 let gen_req = job.request.clone();
170 let progress_tx = job.progress_tx.clone();
171
172 #[cfg(feature = "metrics")]
173 let inference_start = Instant::now();
174 let result = tokio::task::spawn_blocking(move || {
175 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
176 let mut guard = model_cache.blocking_lock();
177 let entry = guard.get_mut(&gen_req.model).ok_or_else(|| {
178 anyhow::anyhow!("no engine available after model readiness check")
179 })?;
180 let e = &mut entry.engine;
181 set_active_generation(&gen_state, &gen_req.model, &gen_req.prompt);
182
183 if let Some(ref ptx) = progress_tx {
185 let ptx = ptx.clone();
186 e.set_on_progress(Box::new(move |event| {
187 let _ = ptx.send(SseMessage::Progress(progress_to_sse(event)));
188 }));
189 } else {
190 e.clear_on_progress();
191 }
192
193 let generate_result = e.generate(&gen_req);
194 if progress_tx.is_some() {
195 e.clear_on_progress();
196 }
197 clear_active_generation(&gen_state);
198 generate_result
199 }))
200 })
201 .await;
202
203 #[cfg(feature = "metrics")]
204 let inference_duration = inference_start.elapsed().as_secs_f64();
205
206 match result {
207 Ok(Ok(Ok(mut response))) => {
208 #[cfg(feature = "metrics")]
209 crate::metrics::record_generation(&job.request.model, inference_duration);
210
211 if response.images.is_empty() && response.video.is_none() {
212 let err_msg = "generation error: engine returned no images or video".to_string();
213 if let Some(ref tx) = job.progress_tx {
214 let _ = tx.send(SseMessage::Error(SseErrorEvent {
215 message: err_msg.clone(),
216 }));
217 }
218 let _ = job.result_tx.send(Err(err_msg));
219 return;
220 }
221 let img = if !response.images.is_empty() {
224 response.images.remove(0)
225 } else if let Some(ref video) = response.video {
226 ImageData {
227 data: video.thumbnail.clone(),
228 format: OutputFormat::Png,
229 width: video.width,
230 height: video.height,
231 index: 0,
232 }
233 } else {
234 unreachable!("checked above");
235 };
236
237 if let Some(ref dir) = job.output_dir {
239 let dir = dir.clone();
240 let model = job.request.model.clone();
241 let batch_size = job.request.batch_size;
242 if let Some(ref video) = response.video {
244 let video_data = video.data.clone();
245 let ext = video.format.extension().to_string();
246 tokio::task::spawn_blocking(move || {
247 let ts = std::time::SystemTime::now()
248 .duration_since(std::time::UNIX_EPOCH)
249 .map(|d| d.as_millis() as u64)
250 .unwrap_or(0);
251 let filename = mold_core::default_output_filename(&model, ts, &ext, 1, 0);
252 let path = std::path::Path::new(&dir).join(filename);
253 if let Err(e) = std::fs::write(&path, &video_data) {
254 tracing::error!("failed to save video to {}: {e}", path.display());
255 }
256 });
257 } else {
258 let img_clone = img.clone();
259 tokio::task::spawn_blocking(move || {
260 save_image_to_dir(&dir, &img_clone, &model, batch_size);
261 });
262 }
263 }
264
265 if let Some(ref tx) = job.progress_tx {
267 let _ = tx.send(SseMessage::Complete(SseCompleteEvent {
268 image: base64::engine::general_purpose::STANDARD.encode(&img.data),
269 format: img.format,
270 width: img.width,
271 height: img.height,
272 seed_used: response.seed_used,
273 generation_time_ms: response.generation_time_ms,
274 model: response.model.clone(),
275 }));
276 }
277
278 let _ = job.result_tx.send(Ok(GenerationJobResult {
280 image: img,
281 response,
282 }));
283 }
284 Ok(Ok(Err(e))) => {
285 #[cfg(feature = "metrics")]
286 crate::metrics::record_generation_error(&job.request.model);
287
288 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
289 tracing::error!("generation error: {e:#}");
290 let err_msg = format!("generation error: {}", clean_error_message(&e));
291 if let Some(ref tx) = job.progress_tx {
292 let _ = tx.send(SseMessage::Error(SseErrorEvent {
293 message: err_msg.clone(),
294 }));
295 }
296 let _ = job.result_tx.send(Err(err_msg));
297 }
298 Ok(Err(panic_payload)) => {
299 #[cfg(feature = "metrics")]
300 crate::metrics::record_generation_error(&job.request.model);
301
302 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
303 let msg = panic_payload
304 .downcast_ref::<String>()
305 .map(|s| s.as_str())
306 .or_else(|| panic_payload.downcast_ref::<&str>().copied())
307 .unwrap_or("unknown panic");
308 tracing::error!("inference panicked: {msg}");
309 let err_msg = format!("inference panicked: {msg}");
310 if let Some(ref tx) = job.progress_tx {
311 let _ = tx.send(SseMessage::Error(SseErrorEvent {
312 message: err_msg.clone(),
313 }));
314 }
315 let _ = job.result_tx.send(Err(err_msg));
316 }
317 Err(join_err) => {
318 #[cfg(feature = "metrics")]
319 crate::metrics::record_generation_error(&job.request.model);
320
321 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
322 tracing::error!("inference task join error: {join_err:?}");
323 let err_msg = "inference task failed".to_string();
324 if let Some(ref tx) = job.progress_tx {
325 let _ = tx.send(SseMessage::Error(SseErrorEvent {
326 message: err_msg.clone(),
327 }));
328 }
329 let _ = job.result_tx.send(Err(err_msg));
330 }
331 }
332}