1use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
10#[serde(rename_all = "snake_case")]
11pub enum TaskKind {
12 Image,
13 Llm,
14 AudioStt,
15 AudioTts,
16 Video,
17}
18
19impl TaskKind {
20 pub const ALL: [TaskKind; 5] = [
21 TaskKind::Image,
22 TaskKind::Llm,
23 TaskKind::AudioStt,
24 TaskKind::AudioTts,
25 TaskKind::Video,
26 ];
27
28 pub fn as_str(&self) -> &'static str {
29 match self {
30 TaskKind::Image => "image",
31 TaskKind::Llm => "llm",
32 TaskKind::AudioStt => "audio_stt",
33 TaskKind::AudioTts => "audio_tts",
34 TaskKind::Video => "video",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct ImageParams {
46 pub prompt: String,
47 #[serde(default)]
51 pub negative_prompt: Option<String>,
52 #[serde(default)]
57 pub init_image_url: Option<String>,
58 #[serde(default)]
63 pub mask_url: Option<String>,
64 #[serde(default)]
72 pub ref_image_url: Option<String>,
73 #[serde(default)]
76 pub denoise: Option<f32>,
77 #[serde(default)]
80 pub cfg_scale: Option<f32>,
81 #[serde(default)]
84 pub sampling_method: Option<String>,
85 #[serde(default = "default_image_dim")]
86 pub width: u32,
87 #[serde(default = "default_image_dim")]
88 pub height: u32,
89 #[serde(default = "default_steps")]
90 pub steps: u32,
91 #[serde(default)]
92 pub seed: Option<u64>,
93 #[serde(default = "default_image_ext")]
94 pub ext: String,
95}
96
97fn default_image_dim() -> u32 {
98 512
99}
100fn default_steps() -> u32 {
101 20
102}
103fn default_image_ext() -> String {
104 "webp".into()
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ChatMessage {
109 pub role: String,
110 pub content: String,
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct LlmParams {
116 pub messages: Vec<ChatMessage>,
117 #[serde(default)]
121 pub system: Option<String>,
122 #[serde(default = "default_max_tokens")]
123 pub max_tokens: u32,
124 #[serde(default = "default_temperature")]
125 pub temperature: f32,
126 #[serde(default)]
127 pub top_p: Option<f32>,
128 #[serde(default)]
129 pub stop: Option<Vec<String>>,
130 #[serde(default)]
133 pub json_schema: Option<serde_json::Value>,
134 #[serde(default)]
137 pub reasoning: Option<String>,
138}
139
140fn default_max_tokens() -> u32 {
141 512
142}
143fn default_temperature() -> f32 {
144 0.7
145}
146
147#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct AudioSttParams {
150 pub input_url: String,
152 #[serde(default)]
153 pub language: Option<String>,
154 #[serde(default)]
156 pub translate: Option<bool>,
157 #[serde(default)]
159 pub prompt: Option<String>,
160 #[serde(default)]
162 pub vad: Option<bool>,
163 #[serde(default)]
165 pub timestamps: Option<String>,
166}
167
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct AudioTtsParams {
171 pub text: String,
172 #[serde(default = "default_voice")]
173 pub voice: String,
174 #[serde(default)]
176 pub speed: Option<f32>,
177 #[serde(default)]
179 pub language: Option<String>,
180 #[serde(default = "default_audio_ext")]
181 pub ext: String,
182}
183
184fn default_voice() -> String {
185 "default".into()
186}
187fn default_audio_ext() -> String {
188 "wav".into()
189}
190
191#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct VideoParams {
194 pub prompt: String,
195 #[serde(default)]
196 pub negative_prompt: Option<String>,
197 #[serde(default)]
199 pub init_image_url: Option<String>,
200 #[serde(default = "default_video_seconds")]
201 pub seconds: f32,
202 #[serde(default)]
204 pub fps: Option<u32>,
205 #[serde(default = "default_image_dim")]
206 pub width: u32,
207 #[serde(default = "default_image_dim")]
208 pub height: u32,
209 #[serde(default = "default_video_ext")]
210 pub ext: String,
211}
212
213fn default_video_seconds() -> f32 {
214 2.0
215}
216fn default_video_ext() -> String {
217 "mp4".into()
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(tag = "kind", rename_all = "snake_case")]
222pub enum Task {
223 Image(ImageParams),
224 Llm(LlmParams),
225 AudioStt(AudioSttParams),
226 AudioTts(AudioTtsParams),
227 Video(VideoParams),
228}
229
230impl Task {
231 pub fn kind(&self) -> TaskKind {
232 match self {
233 Task::Image(_) => TaskKind::Image,
234 Task::Llm(_) => TaskKind::Llm,
235 Task::AudioStt(_) => TaskKind::AudioStt,
236 Task::AudioTts(_) => TaskKind::AudioTts,
237 Task::Video(_) => TaskKind::Video,
238 }
239 }
240}
241
242#[derive(Debug, Clone)]
247pub enum TaskResult {
248 Image { bytes: Vec<u8>, ext: String },
250 Llm { json: serde_json::Value },
252 AudioStt { json: serde_json::Value },
254 AudioTts { bytes: Vec<u8>, ext: String },
256 Video { bytes: Vec<u8>, ext: String },
258}
259
260impl TaskResult {
261 pub fn kind(&self) -> TaskKind {
262 match self {
263 TaskResult::Image { .. } => TaskKind::Image,
264 TaskResult::Llm { .. } => TaskKind::Llm,
265 TaskResult::AudioStt { .. } => TaskKind::AudioStt,
266 TaskResult::AudioTts { .. } => TaskKind::AudioTts,
267 TaskResult::Video { .. } => TaskKind::Video,
268 }
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct WorkerCapabilities {
278 #[serde(rename = "machineName")]
279 pub machine_name: String,
280 pub username: String,
281 #[serde(rename = "agentVersion")]
282 pub agent_version: String,
283 pub engine: String,
284 #[serde(rename = "vramTotalGb")]
285 pub vram_total_gb: f32,
286 #[serde(rename = "vramThresholdGb")]
287 pub vram_threshold_gb: f32,
288 #[serde(rename = "autoEnabled")]
289 pub auto_enabled: bool,
290 #[serde(rename = "autoStart")]
291 pub auto_start: bool,
292 #[serde(rename = "supportedModels")]
296 pub supported_models: Vec<String>,
297 #[serde(rename = "taskKinds", default)]
299 pub task_kinds: Vec<TaskKind>,
300 #[serde(rename = "supportedModelsPerKind", default)]
302 pub supported_models_per_kind: BTreeMap<TaskKind, Vec<String>>,
303}
304
305#[derive(Debug, Clone, Serialize)]
315pub struct AutoRegisterRequest {
316 #[serde(rename = "installId")]
319 pub install_id: String,
320 #[serde(rename = "registrationSecretHash")]
324 pub registration_secret_hash: String,
325 pub capabilities: WorkerCapabilities,
328 #[serde(rename = "userAgent")]
330 pub user_agent: String,
331}
332
333#[derive(Debug, Clone, Deserialize)]
334pub struct AutoRegisterRequestResponse {
335 #[serde(rename = "requestId")]
336 pub request_id: String,
337 pub status: String,
341}
342
343#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
345#[serde(rename_all = "snake_case", tag = "status")]
346pub enum RegisterStatus {
347 Pending,
348 Approved {
349 #[serde(rename = "workerId")]
350 worker_id: String,
351 #[serde(rename = "authToken")]
352 auth_token: String,
353 },
354 Rejected {
355 #[serde(default)]
356 reason: String,
357 },
358}
359
360#[derive(Debug, Clone, Serialize)]
361pub struct HeartbeatRequest {
362 pub capabilities: WorkerCapabilities,
363 #[serde(rename = "currentJobId", skip_serializing_if = "Option::is_none")]
364 pub current_job_id: Option<String>,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "kebab-case")]
375pub enum ModelFileRole {
376 DiffusionModel,
377 TextEncoder,
378 TextEncoderVision,
382 Vae,
383 Lora,
384 Model,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388#[serde(rename_all = "kebab-case")]
389pub enum ModelEngine {
390 SdCpp,
391 LlamaCpp,
392 Onnx,
395 Synthetic,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct ModelFile {
401 pub role: ModelFileRole,
402 pub url: String,
403 pub filename: String,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub approx_bytes: Option<u64>,
406}
407
408#[derive(Debug, Clone, Default, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct ModelCliDefaults {
411 pub cfg_scale: f32,
412 pub steps: u32,
413 pub width: u32,
414 pub height: u32,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub sampling_method: Option<String>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub flow_shift: Option<f32>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub zero_cond_t: Option<bool>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub offload_to_cpu: Option<bool>,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct ModelSource {
431 pub engine: ModelEngine,
432 pub files: Vec<ModelFile>,
433 pub cli_defaults: ModelCliDefaults,
434}
435
436#[derive(Debug, Clone, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct JobClaim {
444 pub job_id: String,
445 #[allow(dead_code)]
446 pub game_id: String,
447 pub asset_name: String,
448 pub model: String,
449 pub vram_gb_estimate: f32,
450 pub task: Task,
454 pub model_source: ModelSource,
458}
459
460#[derive(Debug, Clone, Serialize)]
461pub struct FailRequest {
462 pub error: String,
463 pub retryable: bool,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[cfg_attr(feature = "ui", derive(PartialEq, Eq))]
468pub struct LogEntry {
469 pub ts: String,
470 pub level: String,
471 pub category: String,
472 pub message: String,
473 #[serde(rename = "jobId", default, skip_serializing_if = "Option::is_none")]
474 pub job_id: Option<String>,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct LogBatch {
479 pub entries: Vec<LogEntry>,
480}
481
482#[derive(Debug, Clone, Deserialize)]
488pub struct GithubRelease {
489 pub tag_name: String,
490 #[serde(default)]
491 pub prerelease: bool,
492 #[serde(default)]
493 pub draft: bool,
494 #[serde(default)]
495 pub assets: Vec<GithubReleaseAsset>,
496}
497
498#[derive(Debug, Clone, Deserialize)]
499pub struct GithubReleaseAsset {
500 pub name: String,
501 pub browser_download_url: String,
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 fn synthetic_model_source_json() -> serde_json::Value {
509 serde_json::json!({
510 "engine": "synthetic",
511 "files": [],
512 "cliDefaults": {
513 "cfgScale": 1.0,
514 "steps": 8,
515 "width": 1024,
516 "height": 1024,
517 },
518 })
519 }
520
521 #[test]
522 fn job_claim_requires_task_and_model_source() {
523 let bare = serde_json::json!({
526 "jobId": "j-1",
527 "gameId": "g-1",
528 "assetName": "g-1/creatures/x",
529 "model": "synthetic-image",
530 "vramGbEstimate": 1.0,
531 });
532 assert!(
533 serde_json::from_value::<JobClaim>(bare).is_err(),
534 "JobClaim must reject missing task + modelSource"
535 );
536 }
537
538 #[test]
539 fn job_claim_with_explicit_llm_task() {
540 let json = serde_json::json!({
541 "jobId": "j-2",
542 "gameId": "g-1",
543 "assetName": "g-1/conversations/x",
544 "model": "llama-3.1-8b",
545 "vramGbEstimate": 8.0,
546 "task": {
547 "kind": "llm",
548 "messages": [{"role": "user", "content": "hi"}],
549 "maxTokens": 32,
550 "temperature": 0.5,
551 },
552 "modelSource": synthetic_model_source_json(),
553 });
554 let claim: JobClaim = serde_json::from_value(json).unwrap();
555 match claim.task {
556 Task::Llm(p) => {
557 assert_eq!(p.messages.len(), 1);
558 assert_eq!(p.max_tokens, 32);
559 }
560 other => panic!("expected llm, got {:?}", other),
561 }
562 }
563
564 #[test]
565 fn job_claim_with_explicit_image_task() {
566 let json = serde_json::json!({
567 "jobId": "j-3",
568 "gameId": "g-1",
569 "assetName": "g-1/creatures/y",
570 "model": "synthetic-image",
571 "vramGbEstimate": 8.0,
572 "task": {
573 "kind": "image",
574 "prompt": "a koi",
575 "width": 1024,
576 "height": 1024,
577 "steps": 30,
578 "ext": "png",
579 },
580 "modelSource": synthetic_model_source_json(),
581 });
582 let claim: JobClaim = serde_json::from_value(json).unwrap();
583 match claim.task {
584 Task::Image(p) => {
585 assert_eq!(p.prompt, "a koi");
586 assert_eq!(p.width, 1024);
587 assert_eq!(p.ext, "png");
588 }
589 other => panic!("expected image, got {:?}", other),
590 }
591 }
592
593 #[test]
594 fn image_params_round_trips_with_new_fields() {
595 let json = serde_json::json!({
596 "kind": "image",
597 "prompt": "a stone golem",
598 "negativePrompt": "text, watermark, low quality",
599 "initImageUrl": "https://example.invalid/t2-golem-stone/latest.webp",
600 "denoise": 0.55,
601 "cfgScale": 7.5,
602 "samplingMethod": "dpm++2m",
603 "width": 768,
604 "height": 512,
605 "steps": 30,
606 "seed": 1234,
607 "ext": "webp",
608 });
609 let task: Task = serde_json::from_value(json).unwrap();
610 match task {
611 Task::Image(p) => {
612 assert_eq!(p.prompt, "a stone golem");
613 assert_eq!(
614 p.negative_prompt.as_deref(),
615 Some("text, watermark, low quality")
616 );
617 assert_eq!(
618 p.init_image_url.as_deref(),
619 Some("https://example.invalid/t2-golem-stone/latest.webp")
620 );
621 assert!((p.denoise.unwrap() - 0.55).abs() < 1e-6);
622 assert!((p.cfg_scale.unwrap() - 7.5).abs() < 1e-6);
623 assert_eq!(p.sampling_method.as_deref(), Some("dpm++2m"));
624 assert_eq!(p.width, 768);
625 assert_eq!(p.height, 512);
626 assert_eq!(p.steps, 30);
627 assert_eq!(p.seed, Some(1234));
628 }
629 other => panic!("expected image, got {:?}", other),
630 }
631 }
632
633 #[test]
634 fn image_params_defaults_when_optional_fields_absent() {
635 let json = serde_json::json!({
636 "kind": "image",
637 "prompt": "a fox"
638 });
639 let task: Task = serde_json::from_value(json).unwrap();
640 match task {
641 Task::Image(p) => {
642 assert_eq!(p.prompt, "a fox");
643 assert!(p.negative_prompt.is_none());
644 assert!(p.init_image_url.is_none());
645 assert!(p.denoise.is_none());
646 assert!(p.cfg_scale.is_none());
647 assert!(p.sampling_method.is_none());
648 assert_eq!(p.width, 512);
649 assert_eq!(p.height, 512);
650 assert_eq!(p.steps, 20);
651 assert_eq!(p.ext, "webp");
652 }
653 other => panic!("expected image, got {:?}", other),
654 }
655 }
656
657 #[test]
658 fn task_kinds_round_trip_via_json() {
659 for kind in TaskKind::ALL {
660 let s = serde_json::to_string(&kind).unwrap();
661 let back: TaskKind = serde_json::from_str(&s).unwrap();
662 assert_eq!(kind, back);
663 }
664 }
665}