1use std::sync::Arc;
2
3use base64::Engine as _;
4use mold_core::{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 process_job(&state, job).await;
110 state.queue.decrement();
111 }
112 tracing::info!("generation queue worker shutting down");
113}
114
115async fn process_job(state: &AppState, job: GenerationJob) {
116 if job.result_tx.is_closed() {
118 tracing::debug!("skipping queued job — client disconnected");
119 return;
120 }
121
122 if let Some(ref tx) = job.progress_tx {
124 let _ = tx.send(SseMessage::Progress(SseProgressEvent::Queued {
125 position: 0,
126 }));
127 }
128
129 let progress_callback = job.progress_tx.as_ref().map(|tx| {
131 let tx = tx.clone();
132 Arc::new(move |event: mold_inference::ProgressEvent| {
133 let _ = tx.send(SseMessage::Progress(progress_to_sse(event)));
134 }) as model_manager::EngineProgressCallback
135 });
136
137 if let Err(api_err) =
138 model_manager::ensure_model_ready(state, &job.request.model, progress_callback).await
139 {
140 let err_msg = api_err.error.clone();
141 if let Some(ref tx) = job.progress_tx {
142 let _ = tx.send(SseMessage::Error(SseErrorEvent {
143 message: err_msg.clone(),
144 }));
145 }
146 let _ = job.result_tx.send(Err(err_msg));
147 return;
148 }
149
150 #[cfg(target_os = "macos")]
152 if let Some(available) = mold_inference::device::available_system_memory_bytes() {
153 if available < 1_000_000_000 {
154 tracing::warn!(
155 available_mb = available / 1_000_000,
156 "low memory before inference — system may become unstable"
157 );
158 }
159 }
160
161 let model_cache = state.model_cache.clone();
163 let active_gen = state.active_generation.clone();
164 let gen_state = state.clone();
165 let gen_req = job.request.clone();
166 let progress_tx = job.progress_tx.clone();
167
168 let result = tokio::task::spawn_blocking(move || {
169 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
170 let mut guard = model_cache.blocking_lock();
171 let entry = guard.get_mut(&gen_req.model).ok_or_else(|| {
172 anyhow::anyhow!("no engine available after model readiness check")
173 })?;
174 let e = &mut entry.engine;
175 set_active_generation(&gen_state, &gen_req.model, &gen_req.prompt);
176
177 if let Some(ref ptx) = progress_tx {
179 let ptx = ptx.clone();
180 e.set_on_progress(Box::new(move |event| {
181 let _ = ptx.send(SseMessage::Progress(progress_to_sse(event)));
182 }));
183 } else {
184 e.clear_on_progress();
185 }
186
187 let generate_result = e.generate(&gen_req);
188 if progress_tx.is_some() {
189 e.clear_on_progress();
190 }
191 clear_active_generation(&gen_state);
192 generate_result
193 }))
194 })
195 .await;
196
197 match result {
198 Ok(Ok(Ok(mut response))) => {
199 if response.images.is_empty() {
200 let err_msg = "generation error: engine returned no images".to_string();
201 if let Some(ref tx) = job.progress_tx {
202 let _ = tx.send(SseMessage::Error(SseErrorEvent {
203 message: err_msg.clone(),
204 }));
205 }
206 let _ = job.result_tx.send(Err(err_msg));
207 return;
208 }
209 let img = response.images.remove(0);
210
211 if let Some(ref dir) = job.output_dir {
213 let dir = dir.clone();
214 let img_clone = img.clone();
215 let model = job.request.model.clone();
216 let batch_size = job.request.batch_size;
217 tokio::task::spawn_blocking(move || {
218 save_image_to_dir(&dir, &img_clone, &model, batch_size);
219 });
220 }
221
222 if let Some(ref tx) = job.progress_tx {
224 let _ = tx.send(SseMessage::Complete(SseCompleteEvent {
225 image: base64::engine::general_purpose::STANDARD.encode(&img.data),
226 format: img.format,
227 width: img.width,
228 height: img.height,
229 seed_used: response.seed_used,
230 generation_time_ms: response.generation_time_ms,
231 model: response.model.clone(),
232 }));
233 }
234
235 let _ = job.result_tx.send(Ok(GenerationJobResult {
237 image: img,
238 response,
239 }));
240 }
241 Ok(Ok(Err(e))) => {
242 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
243 tracing::error!("generation error: {e:#}");
244 let err_msg = format!("generation error: {}", clean_error_message(&e));
245 if let Some(ref tx) = job.progress_tx {
246 let _ = tx.send(SseMessage::Error(SseErrorEvent {
247 message: err_msg.clone(),
248 }));
249 }
250 let _ = job.result_tx.send(Err(err_msg));
251 }
252 Ok(Err(panic_payload)) => {
253 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
254 let msg = panic_payload
255 .downcast_ref::<String>()
256 .map(|s| s.as_str())
257 .or_else(|| panic_payload.downcast_ref::<&str>().copied())
258 .unwrap_or("unknown panic");
259 tracing::error!("inference panicked: {msg}");
260 let err_msg = format!("inference panicked: {msg}");
261 if let Some(ref tx) = job.progress_tx {
262 let _ = tx.send(SseMessage::Error(SseErrorEvent {
263 message: err_msg.clone(),
264 }));
265 }
266 let _ = job.result_tx.send(Err(err_msg));
267 }
268 Err(join_err) => {
269 *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
270 tracing::error!("inference task join error: {join_err:?}");
271 let err_msg = "inference task failed".to_string();
272 if let Some(ref tx) = job.progress_tx {
273 let _ = tx.send(SseMessage::Error(SseErrorEvent {
274 message: err_msg.clone(),
275 }));
276 }
277 let _ = job.result_tx.send(Err(err_msg));
278 }
279 }
280}