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 b64 = base64::engine::general_purpose::STANDARD;
268 let event = if let Some(ref video) = response.video {
269 SseCompleteEvent {
271 image: b64.encode(&video.data),
272 format: video.format,
273 width: video.width,
274 height: video.height,
275 seed_used: response.seed_used,
276 generation_time_ms: response.generation_time_ms,
277 model: response.model.clone(),
278 video_frames: Some(video.frames),
279 video_fps: Some(video.fps),
280 video_thumbnail: Some(b64.encode(&video.thumbnail)),
281 video_gif_preview: if video.gif_preview.is_empty() {
282 None
283 } else {
284 Some(b64.encode(&video.gif_preview))
285 },
286 video_has_audio: video.has_audio,
287 video_duration_ms: video.duration_ms,
288 video_audio_sample_rate: video.audio_sample_rate,
289 video_audio_channels: video.audio_channels,
290 }
291 } else {
292 SseCompleteEvent {
294 image: b64.encode(&img.data),
295 format: img.format,
296 width: img.width,
297 height: img.height,
298 seed_used: response.seed_used,
299 generation_time_ms: response.generation_time_ms,
300 model: response.model.clone(),
301 video_frames: None,
302 video_fps: None,
303 video_thumbnail: None,
304 video_gif_preview: None,
305 video_has_audio: false,
306 video_duration_ms: None,
307 video_audio_sample_rate: None,
308 video_audio_channels: None,
309 }
310 };
311 let _ = tx.send(SseMessage::Complete(event));
312 }
313
314 let _ = job.result_tx.send(Ok(GenerationJobResult {
316 image: img,
317 response,
318 }));
319 }
320 Ok(Ok(Err(e))) => {
321 #[cfg(feature = "metrics")]
322 crate::metrics::record_generation_error(&job.request.model);
323
324 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
325 tracing::error!("generation error: {e:#}");
326 let err_msg = format!("generation error: {}", clean_error_message(&e));
327 if let Some(ref tx) = job.progress_tx {
328 let _ = tx.send(SseMessage::Error(SseErrorEvent {
329 message: err_msg.clone(),
330 }));
331 }
332 let _ = job.result_tx.send(Err(err_msg));
333 }
334 Ok(Err(panic_payload)) => {
335 #[cfg(feature = "metrics")]
336 crate::metrics::record_generation_error(&job.request.model);
337
338 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
339 let msg = panic_payload
340 .downcast_ref::<String>()
341 .map(|s| s.as_str())
342 .or_else(|| panic_payload.downcast_ref::<&str>().copied())
343 .unwrap_or("unknown panic");
344 tracing::error!("inference panicked: {msg}");
345 let err_msg = format!("inference panicked: {msg}");
346 if let Some(ref tx) = job.progress_tx {
347 let _ = tx.send(SseMessage::Error(SseErrorEvent {
348 message: err_msg.clone(),
349 }));
350 }
351 let _ = job.result_tx.send(Err(err_msg));
352 }
353 Err(join_err) => {
354 #[cfg(feature = "metrics")]
355 crate::metrics::record_generation_error(&job.request.model);
356
357 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
358 tracing::error!("inference task join error: {join_err:?}");
359 let err_msg = "inference task failed".to_string();
360 if let Some(ref tx) = job.progress_tx {
361 let _ = tx.send(SseMessage::Error(SseErrorEvent {
362 message: err_msg.clone(),
363 }));
364 }
365 let _ = job.result_tx.send(Err(err_msg));
366 }
367 }
368}