@sogni-ai/expo-client 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +411 -0
  3. package/dist/cjs/Account/index.d.ts +60 -0
  4. package/dist/cjs/Account/index.d.ts.map +1 -0
  5. package/dist/cjs/Account/index.js +188 -0
  6. package/dist/cjs/Account/index.js.map +1 -0
  7. package/dist/cjs/Account/types.d.ts +43 -0
  8. package/dist/cjs/Account/types.d.ts.map +1 -0
  9. package/dist/cjs/Account/types.js +18 -0
  10. package/dist/cjs/Account/types.js.map +1 -0
  11. package/dist/cjs/ApiClient/WebSocketClient/events.d.ts +139 -0
  12. package/dist/cjs/ApiClient/WebSocketClient/events.d.ts.map +1 -0
  13. package/dist/cjs/ApiClient/WebSocketClient/events.js +3 -0
  14. package/dist/cjs/ApiClient/WebSocketClient/events.js.map +1 -0
  15. package/dist/cjs/ApiClient/WebSocketClient/index.d.ts +45 -0
  16. package/dist/cjs/ApiClient/WebSocketClient/index.d.ts.map +1 -0
  17. package/dist/cjs/ApiClient/WebSocketClient/index.js +283 -0
  18. package/dist/cjs/ApiClient/WebSocketClient/index.js.map +1 -0
  19. package/dist/cjs/ApiClient/WebSocketClient/normalize.d.ts +45 -0
  20. package/dist/cjs/ApiClient/WebSocketClient/normalize.d.ts.map +1 -0
  21. package/dist/cjs/ApiClient/WebSocketClient/normalize.js +245 -0
  22. package/dist/cjs/ApiClient/WebSocketClient/normalize.js.map +1 -0
  23. package/dist/cjs/ApiClient/WebSocketClient/types.d.ts +9 -0
  24. package/dist/cjs/ApiClient/WebSocketClient/types.d.ts.map +1 -0
  25. package/dist/cjs/ApiClient/WebSocketClient/types.js +3 -0
  26. package/dist/cjs/ApiClient/WebSocketClient/types.js.map +1 -0
  27. package/dist/cjs/ApiClient/WebSocketClient/wireTypes.d.ts +161 -0
  28. package/dist/cjs/ApiClient/WebSocketClient/wireTypes.d.ts.map +1 -0
  29. package/dist/cjs/ApiClient/WebSocketClient/wireTypes.js +11 -0
  30. package/dist/cjs/ApiClient/WebSocketClient/wireTypes.js.map +1 -0
  31. package/dist/cjs/ApiClient/index.d.ts +60 -0
  32. package/dist/cjs/ApiClient/index.d.ts.map +1 -0
  33. package/dist/cjs/ApiClient/index.js +87 -0
  34. package/dist/cjs/ApiClient/index.js.map +1 -0
  35. package/dist/cjs/Chat/ChatStream.d.ts +42 -0
  36. package/dist/cjs/Chat/ChatStream.d.ts.map +1 -0
  37. package/dist/cjs/Chat/ChatStream.js +146 -0
  38. package/dist/cjs/Chat/ChatStream.js.map +1 -0
  39. package/dist/cjs/Chat/index.d.ts +61 -0
  40. package/dist/cjs/Chat/index.d.ts.map +1 -0
  41. package/dist/cjs/Chat/index.js +179 -0
  42. package/dist/cjs/Chat/index.js.map +1 -0
  43. package/dist/cjs/Chat/tools.d.ts +13 -0
  44. package/dist/cjs/Chat/tools.d.ts.map +1 -0
  45. package/dist/cjs/Chat/tools.js +22 -0
  46. package/dist/cjs/Chat/tools.js.map +1 -0
  47. package/dist/cjs/Chat/types.d.ts +153 -0
  48. package/dist/cjs/Chat/types.d.ts.map +1 -0
  49. package/dist/cjs/Chat/types.js +3 -0
  50. package/dist/cjs/Chat/types.js.map +1 -0
  51. package/dist/cjs/Projects/ControlNet.d.ts +30 -0
  52. package/dist/cjs/Projects/ControlNet.d.ts.map +1 -0
  53. package/dist/cjs/Projects/ControlNet.js +45 -0
  54. package/dist/cjs/Projects/ControlNet.js.map +1 -0
  55. package/dist/cjs/Projects/createJobRequestMessage.d.ts +11 -0
  56. package/dist/cjs/Projects/createJobRequestMessage.d.ts.map +1 -0
  57. package/dist/cjs/Projects/createJobRequestMessage.js +239 -0
  58. package/dist/cjs/Projects/createJobRequestMessage.js.map +1 -0
  59. package/dist/cjs/Projects/index.d.ts +96 -0
  60. package/dist/cjs/Projects/index.d.ts.map +1 -0
  61. package/dist/cjs/Projects/index.js +536 -0
  62. package/dist/cjs/Projects/index.js.map +1 -0
  63. package/dist/cjs/Projects/restNormalize.d.ts +60 -0
  64. package/dist/cjs/Projects/restNormalize.d.ts.map +1 -0
  65. package/dist/cjs/Projects/restNormalize.js +89 -0
  66. package/dist/cjs/Projects/restNormalize.js.map +1 -0
  67. package/dist/cjs/Projects/types.d.ts +233 -0
  68. package/dist/cjs/Projects/types.d.ts.map +1 -0
  69. package/dist/cjs/Projects/types.js +3 -0
  70. package/dist/cjs/Projects/types.js.map +1 -0
  71. package/dist/cjs/Projects/utils.d.ts +23 -0
  72. package/dist/cjs/Projects/utils.d.ts.map +1 -0
  73. package/dist/cjs/Projects/utils.js +65 -0
  74. package/dist/cjs/Projects/utils.js.map +1 -0
  75. package/dist/cjs/index.d.ts +63 -0
  76. package/dist/cjs/index.d.ts.map +1 -0
  77. package/dist/cjs/index.js +116 -0
  78. package/dist/cjs/index.js.map +1 -0
  79. package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.d.ts +15 -0
  80. package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.d.ts.map +1 -0
  81. package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.js +47 -0
  82. package/dist/cjs/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
  83. package/dist/cjs/lib/AuthManager/AuthManagerBase.d.ts +23 -0
  84. package/dist/cjs/lib/AuthManager/AuthManagerBase.d.ts.map +1 -0
  85. package/dist/cjs/lib/AuthManager/AuthManagerBase.js +14 -0
  86. package/dist/cjs/lib/AuthManager/AuthManagerBase.js.map +1 -0
  87. package/dist/cjs/lib/AuthManager/AuthStorage.d.ts +11 -0
  88. package/dist/cjs/lib/AuthManager/AuthStorage.d.ts.map +1 -0
  89. package/dist/cjs/lib/AuthManager/AuthStorage.js +50 -0
  90. package/dist/cjs/lib/AuthManager/AuthStorage.js.map +1 -0
  91. package/dist/cjs/lib/AuthManager/TokenAuthManager.d.ts +35 -0
  92. package/dist/cjs/lib/AuthManager/TokenAuthManager.d.ts.map +1 -0
  93. package/dist/cjs/lib/AuthManager/TokenAuthManager.js +164 -0
  94. package/dist/cjs/lib/AuthManager/TokenAuthManager.js.map +1 -0
  95. package/dist/cjs/lib/AuthManager/index.d.ts +7 -0
  96. package/dist/cjs/lib/AuthManager/index.d.ts.map +1 -0
  97. package/dist/cjs/lib/AuthManager/index.js +13 -0
  98. package/dist/cjs/lib/AuthManager/index.js.map +1 -0
  99. package/dist/cjs/lib/Cache.d.ts +12 -0
  100. package/dist/cjs/lib/Cache.d.ts.map +1 -0
  101. package/dist/cjs/lib/Cache.js +36 -0
  102. package/dist/cjs/lib/Cache.js.map +1 -0
  103. package/dist/cjs/lib/DefaultLogger.d.ts +17 -0
  104. package/dist/cjs/lib/DefaultLogger.d.ts.map +1 -0
  105. package/dist/cjs/lib/DefaultLogger.js +36 -0
  106. package/dist/cjs/lib/DefaultLogger.js.map +1 -0
  107. package/dist/cjs/lib/RestClient.d.ts +18 -0
  108. package/dist/cjs/lib/RestClient.d.ts.map +1 -0
  109. package/dist/cjs/lib/RestClient.js +67 -0
  110. package/dist/cjs/lib/RestClient.js.map +1 -0
  111. package/dist/cjs/lib/TypedEventEmitter.d.ts +16 -0
  112. package/dist/cjs/lib/TypedEventEmitter.d.ts.map +1 -0
  113. package/dist/cjs/lib/TypedEventEmitter.js +46 -0
  114. package/dist/cjs/lib/TypedEventEmitter.js.map +1 -0
  115. package/dist/cjs/lib/base64.d.ts +3 -0
  116. package/dist/cjs/lib/base64.d.ts.map +1 -0
  117. package/dist/cjs/lib/base64.js +21 -0
  118. package/dist/cjs/lib/base64.js.map +1 -0
  119. package/dist/cjs/lib/getUUID.d.ts +12 -0
  120. package/dist/cjs/lib/getUUID.d.ts.map +1 -0
  121. package/dist/cjs/lib/getUUID.js +29 -0
  122. package/dist/cjs/lib/getUUID.js.map +1 -0
  123. package/dist/cjs/lib/mediaInput.d.ts +33 -0
  124. package/dist/cjs/lib/mediaInput.d.ts.map +1 -0
  125. package/dist/cjs/lib/mediaInput.js +32 -0
  126. package/dist/cjs/lib/mediaInput.js.map +1 -0
  127. package/dist/cjs/lib/utils.d.ts +10 -0
  128. package/dist/cjs/lib/utils.d.ts.map +1 -0
  129. package/dist/cjs/lib/utils.js +24 -0
  130. package/dist/cjs/lib/utils.js.map +1 -0
  131. package/dist/cjs/types/ErrorData.d.ts +7 -0
  132. package/dist/cjs/types/ErrorData.d.ts.map +1 -0
  133. package/dist/cjs/types/ErrorData.js +3 -0
  134. package/dist/cjs/types/ErrorData.js.map +1 -0
  135. package/dist/cjs/types/json.d.ts +6 -0
  136. package/dist/cjs/types/json.d.ts.map +1 -0
  137. package/dist/cjs/types/json.js +3 -0
  138. package/dist/cjs/types/json.js.map +1 -0
  139. package/dist/cjs/types/token.d.ts +2 -0
  140. package/dist/cjs/types/token.d.ts.map +1 -0
  141. package/dist/cjs/types/token.js +3 -0
  142. package/dist/cjs/types/token.js.map +1 -0
  143. package/dist/cjs/version.d.ts +2 -0
  144. package/dist/cjs/version.d.ts.map +1 -0
  145. package/dist/cjs/version.js +5 -0
  146. package/dist/cjs/version.js.map +1 -0
  147. package/dist/cjs/wallet.d.ts +59 -0
  148. package/dist/cjs/wallet.d.ts.map +1 -0
  149. package/dist/cjs/wallet.js +125 -0
  150. package/dist/cjs/wallet.js.map +1 -0
  151. package/dist/esm/Account/index.js +183 -0
  152. package/dist/esm/Account/index.js.map +1 -0
  153. package/dist/esm/Account/types.js +15 -0
  154. package/dist/esm/Account/types.js.map +1 -0
  155. package/dist/esm/ApiClient/WebSocketClient/events.js +2 -0
  156. package/dist/esm/ApiClient/WebSocketClient/events.js.map +1 -0
  157. package/dist/esm/ApiClient/WebSocketClient/index.js +278 -0
  158. package/dist/esm/ApiClient/WebSocketClient/index.js.map +1 -0
  159. package/dist/esm/ApiClient/WebSocketClient/normalize.js +229 -0
  160. package/dist/esm/ApiClient/WebSocketClient/normalize.js.map +1 -0
  161. package/dist/esm/ApiClient/WebSocketClient/types.js +2 -0
  162. package/dist/esm/ApiClient/WebSocketClient/types.js.map +1 -0
  163. package/dist/esm/ApiClient/WebSocketClient/wireTypes.js +10 -0
  164. package/dist/esm/ApiClient/WebSocketClient/wireTypes.js.map +1 -0
  165. package/dist/esm/ApiClient/index.js +80 -0
  166. package/dist/esm/ApiClient/index.js.map +1 -0
  167. package/dist/esm/Chat/ChatStream.js +144 -0
  168. package/dist/esm/Chat/ChatStream.js.map +1 -0
  169. package/dist/esm/Chat/index.js +174 -0
  170. package/dist/esm/Chat/index.js.map +1 -0
  171. package/dist/esm/Chat/tools.js +19 -0
  172. package/dist/esm/Chat/tools.js.map +1 -0
  173. package/dist/esm/Chat/types.js +2 -0
  174. package/dist/esm/Chat/types.js.map +1 -0
  175. package/dist/esm/Projects/ControlNet.js +41 -0
  176. package/dist/esm/Projects/ControlNet.js.map +1 -0
  177. package/dist/esm/Projects/createJobRequestMessage.js +236 -0
  178. package/dist/esm/Projects/createJobRequestMessage.js.map +1 -0
  179. package/dist/esm/Projects/index.js +531 -0
  180. package/dist/esm/Projects/index.js.map +1 -0
  181. package/dist/esm/Projects/restNormalize.js +86 -0
  182. package/dist/esm/Projects/restNormalize.js.map +1 -0
  183. package/dist/esm/Projects/types.js +2 -0
  184. package/dist/esm/Projects/types.js.map +1 -0
  185. package/dist/esm/Projects/utils.js +55 -0
  186. package/dist/esm/Projects/utils.js.map +1 -0
  187. package/dist/esm/index.js +72 -0
  188. package/dist/esm/index.js.map +1 -0
  189. package/dist/esm/lib/AuthManager/ApiKeyAuthManager.js +42 -0
  190. package/dist/esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
  191. package/dist/esm/lib/AuthManager/AuthManagerBase.js +9 -0
  192. package/dist/esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
  193. package/dist/esm/lib/AuthManager/AuthStorage.js +13 -0
  194. package/dist/esm/lib/AuthManager/AuthStorage.js.map +1 -0
  195. package/dist/esm/lib/AuthManager/TokenAuthManager.js +159 -0
  196. package/dist/esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
  197. package/dist/esm/lib/AuthManager/index.js +5 -0
  198. package/dist/esm/lib/AuthManager/index.js.map +1 -0
  199. package/dist/esm/lib/Cache.js +33 -0
  200. package/dist/esm/lib/Cache.js.map +1 -0
  201. package/dist/esm/lib/DefaultLogger.js +32 -0
  202. package/dist/esm/lib/DefaultLogger.js.map +1 -0
  203. package/dist/esm/lib/RestClient.js +62 -0
  204. package/dist/esm/lib/RestClient.js.map +1 -0
  205. package/dist/esm/lib/TypedEventEmitter.js +44 -0
  206. package/dist/esm/lib/TypedEventEmitter.js.map +1 -0
  207. package/dist/esm/lib/base64.js +17 -0
  208. package/dist/esm/lib/base64.js.map +1 -0
  209. package/dist/esm/lib/getUUID.js +26 -0
  210. package/dist/esm/lib/getUUID.js.map +1 -0
  211. package/dist/esm/lib/mediaInput.js +28 -0
  212. package/dist/esm/lib/mediaInput.js.map +1 -0
  213. package/dist/esm/lib/utils.js +19 -0
  214. package/dist/esm/lib/utils.js.map +1 -0
  215. package/dist/esm/types/ErrorData.js +2 -0
  216. package/dist/esm/types/ErrorData.js.map +1 -0
  217. package/dist/esm/types/json.js +2 -0
  218. package/dist/esm/types/json.js.map +1 -0
  219. package/dist/esm/types/token.js +2 -0
  220. package/dist/esm/types/token.js.map +1 -0
  221. package/dist/esm/version.js +2 -0
  222. package/dist/esm/version.js.map +1 -0
  223. package/dist/esm/wallet.js +117 -0
  224. package/dist/esm/wallet.js.map +1 -0
  225. package/package.json +92 -0
  226. package/src/Account/index.ts +214 -0
  227. package/src/Account/types.ts +61 -0
  228. package/src/ApiClient/WebSocketClient/events.ts +168 -0
  229. package/src/ApiClient/WebSocketClient/index.ts +345 -0
  230. package/src/ApiClient/WebSocketClient/normalize.ts +287 -0
  231. package/src/ApiClient/WebSocketClient/types.ts +10 -0
  232. package/src/ApiClient/WebSocketClient/wireTypes.ts +183 -0
  233. package/src/ApiClient/index.ts +147 -0
  234. package/src/Chat/ChatStream.ts +164 -0
  235. package/src/Chat/index.ts +224 -0
  236. package/src/Chat/tools.ts +23 -0
  237. package/src/Chat/types.ts +165 -0
  238. package/src/Projects/ControlNet.ts +84 -0
  239. package/src/Projects/createJobRequestMessage.ts +241 -0
  240. package/src/Projects/index.ts +608 -0
  241. package/src/Projects/restNormalize.ts +154 -0
  242. package/src/Projects/types.ts +266 -0
  243. package/src/Projects/utils.ts +60 -0
  244. package/src/index.ts +217 -0
  245. package/src/lib/AuthManager/ApiKeyAuthManager.ts +52 -0
  246. package/src/lib/AuthManager/AuthManagerBase.ts +35 -0
  247. package/src/lib/AuthManager/AuthStorage.ts +21 -0
  248. package/src/lib/AuthManager/TokenAuthManager.ts +179 -0
  249. package/src/lib/AuthManager/index.ts +8 -0
  250. package/src/lib/Cache.ts +45 -0
  251. package/src/lib/DefaultLogger.ts +47 -0
  252. package/src/lib/RestClient.ts +77 -0
  253. package/src/lib/TypedEventEmitter.ts +52 -0
  254. package/src/lib/base64.ts +17 -0
  255. package/src/lib/getUUID.ts +27 -0
  256. package/src/lib/mediaInput.ts +54 -0
  257. package/src/lib/utils.ts +21 -0
  258. package/src/types/ErrorData.ts +7 -0
  259. package/src/types/json.ts +5 -0
  260. package/src/types/token.ts +1 -0
  261. package/src/version.ts +1 -0
  262. package/src/wallet.ts +158 -0
@@ -0,0 +1,608 @@
1
+ import ApiClient, { ApiError, ApiResponse } from '../ApiClient';
2
+ import { SupernetType } from '../ApiClient/WebSocketClient/types';
3
+ import {
4
+ ArtistCancelConfirmationEvent,
5
+ JobErrorEvent,
6
+ JobETAEvent,
7
+ JobInitiatingModelEvent,
8
+ JobLifecycleCompletedEvent,
9
+ JobProgressEvent,
10
+ JobResultEvent,
11
+ JobStartedEvent
12
+ } from '../ApiClient/WebSocketClient/events';
13
+ import TypedEventEmitter from '../lib/TypedEventEmitter';
14
+ import getUUID from '../lib/getUUID';
15
+ import { MediaInput, toFetchBody } from '../lib/mediaInput';
16
+ import ErrorData from '../types/ErrorData';
17
+ import createJobRequestMessage from './createJobRequestMessage';
18
+ import {
19
+ AvailableModel,
20
+ ImageProjectParams,
21
+ Job,
22
+ JobStatus,
23
+ Project,
24
+ ProjectParams,
25
+ ProjectStatus,
26
+ VideoProjectParams
27
+ } from './types';
28
+ import { getMaxContextImages } from './ControlNet';
29
+ import { RawProject, rawProjectToProject } from './restNormalize';
30
+
31
+ interface ProjectsApiEvents {
32
+ /** Fired for any project state change (status, progress, jobs, etc.). */
33
+ changed: Project;
34
+ /** Progress tick on any project. */
35
+ progress: { project: Project; jobId: string };
36
+ /** Project reached terminal state 'completed' with all result URLs populated. */
37
+ completed: Project;
38
+ /** Project reached terminal state 'failed' or 'canceled'. */
39
+ failed: Project;
40
+ }
41
+
42
+ const TERMINAL_PROJECT_STATUSES: ProjectStatus[] = ['completed', 'failed', 'canceled'];
43
+ const TERMINAL_JOB_STATUSES: JobStatus[] = ['completed', 'failed', 'canceled'];
44
+ const GC_DELAY_MS = 30_000;
45
+
46
+ class ProjectsApi extends TypedEventEmitter<ProjectsApiEvents> {
47
+ private _client: ApiClient;
48
+ private _projects = new Map<string, Project>();
49
+ private _subscribers = new Map<string, Set<(p: Project) => void>>();
50
+ private _gcTimers = new Map<string, ReturnType<typeof setTimeout>>();
51
+
52
+ constructor(client: ApiClient) {
53
+ super();
54
+ this._client = client;
55
+ this._wireSocketHandlers();
56
+ }
57
+
58
+ /** List models available on the given network. Defaults to the client's current network. */
59
+ async getAvailableModels(network?: SupernetType): Promise<AvailableModel[]> {
60
+ const net = network ?? this._client.socket.supernetType;
61
+ const res = await this._client.socketRest.get<ApiResponse<unknown>>(
62
+ `/api/v1/status/network/${net}/models`
63
+ );
64
+ // The endpoint returns either an array of models directly or wrapped in
65
+ // { models: [...] }. Accept either shape.
66
+ const data: any = res.data;
67
+ if (Array.isArray(data)) return data as AvailableModel[];
68
+ if (data && Array.isArray(data.models)) return data.models as AvailableModel[];
69
+ return [];
70
+ }
71
+
72
+ /**
73
+ * Create a generation project (image, video, or audio).
74
+ *
75
+ * Returns the initial Project snapshot immediately; subscribe via
76
+ * `subscribe(id, cb)` or `waitForCompletion(id)` to observe state changes as
77
+ * the server processes it.
78
+ *
79
+ * For image projects, `startingImage`, `contextImages[]`, and
80
+ * `controlNet.image` are uploaded first. For video projects, `referenceImage`,
81
+ * `referenceImageEnd`, `referenceAudio`, and `referenceVideo` are uploaded
82
+ * first. Reference uploads happen in parallel; the WS `jobRequest` is sent
83
+ * once all uploads complete.
84
+ */
85
+ async create(params: ProjectParams): Promise<Project> {
86
+ if (!this._client.socket.isConnected) {
87
+ throw new Error(
88
+ 'Cannot create project: WebSocket is not connected. Authenticate first and wait for `connected` event.'
89
+ );
90
+ }
91
+ const id = getUUID();
92
+ const project: Project = {
93
+ id,
94
+ params,
95
+ status: 'pending',
96
+ progress: 0,
97
+ eta: null,
98
+ jobs: [],
99
+ resultUrls: [],
100
+ error: null,
101
+ createdAt: Date.now(),
102
+ updatedAt: Date.now()
103
+ };
104
+ this._projects.set(id, project);
105
+
106
+ const type = params.type ?? 'image';
107
+ if (type === 'image') {
108
+ await this._uploadImageRefs(id, params as ImageProjectParams);
109
+ } else if (type === 'video') {
110
+ await this._uploadVideoRefs(id, params as VideoProjectParams);
111
+ }
112
+
113
+ const message = createJobRequestMessage(id, params);
114
+ await this._client.socket.send('jobRequest', message);
115
+ this._notifyChanged(project);
116
+ return project;
117
+ }
118
+
119
+ private async _uploadImageRefs(projectId: string, params: ImageProjectParams) {
120
+ if (params.startingImage) {
121
+ await this._uploadMedia(projectId, 'startingImage', params.startingImage);
122
+ }
123
+ if (params.contextImages && params.contextImages.length) {
124
+ const max = getMaxContextImages(params.modelId);
125
+ if (params.contextImages.length > max) {
126
+ throw new Error(
127
+ `Model ${params.modelId} accepts at most ${max} context images (got ${params.contextImages.length})`
128
+ );
129
+ }
130
+ await Promise.all(
131
+ params.contextImages.map((img, idx) =>
132
+ this._uploadMedia(projectId, `contextImage${idx + 1}`, img)
133
+ )
134
+ );
135
+ }
136
+ if (params.controlNet?.image) {
137
+ await this._uploadMedia(projectId, 'cnImage', params.controlNet.image);
138
+ }
139
+ }
140
+
141
+ private async _uploadVideoRefs(projectId: string, params: VideoProjectParams) {
142
+ const uploads: Promise<unknown>[] = [];
143
+ if (params.referenceImage) {
144
+ uploads.push(this._uploadMedia(projectId, 'referenceImage', params.referenceImage));
145
+ }
146
+ if (params.referenceImageEnd) {
147
+ uploads.push(this._uploadMedia(projectId, 'referenceImageEnd', params.referenceImageEnd));
148
+ }
149
+ if (params.referenceAudio) {
150
+ uploads.push(this._uploadMedia(projectId, 'referenceAudio', params.referenceAudio));
151
+ }
152
+ if (params.referenceVideo) {
153
+ uploads.push(this._uploadMedia(projectId, 'referenceVideo', params.referenceVideo));
154
+ }
155
+ if (uploads.length) await Promise.all(uploads);
156
+ }
157
+
158
+ private async _uploadMedia(projectId: string, type: string, media: MediaInput): Promise<string> {
159
+ const imageId = getUUID();
160
+ const { body, contentType } = await toFetchBody(media);
161
+ const presignedUrl = await this._getUploadUrl({
162
+ imageId,
163
+ jobId: projectId,
164
+ type,
165
+ ...(contentType ? { contentType } : {})
166
+ });
167
+ const headers: Record<string, string> = {};
168
+ if (contentType) headers['Content-Type'] = contentType;
169
+ const res = await fetch(presignedUrl, {
170
+ method: 'PUT',
171
+ headers,
172
+ body
173
+ });
174
+ if (!res.ok) {
175
+ throw new ApiError(res.status, {
176
+ status: 'error',
177
+ errorCode: 0,
178
+ message: `Failed to upload ${type} (HTTP ${res.status})`
179
+ });
180
+ }
181
+ return imageId;
182
+ }
183
+
184
+ private async _getUploadUrl(params: {
185
+ imageId: string;
186
+ jobId: string;
187
+ type: string;
188
+ contentType?: string;
189
+ }): Promise<string> {
190
+ const res = await this._client.socketRest.get<ApiResponse<string | { url: string }>>(
191
+ '/v1/image/uploadUrl',
192
+ params
193
+ );
194
+ const d: any = res.data;
195
+ return typeof d === 'string' ? d : d.url;
196
+ }
197
+
198
+ /** Get the cached snapshot for a project, or undefined if unknown. */
199
+ get(projectId: string): Project | undefined {
200
+ return this._projects.get(projectId.toUpperCase());
201
+ }
202
+
203
+ /**
204
+ * Fetch a project's current state from REST and cache it.
205
+ *
206
+ * Use this on cold launch when you have a persisted project ID but no
207
+ * in-memory snapshot (e.g., the app was killed mid-generation). The server
208
+ * returns the latest known state; if the project is still in flight, the
209
+ * SDK will keep updating the snapshot via socket events.
210
+ *
211
+ * @param projectId Server project ID.
212
+ * @param paramsOverride Optional. The original ProjectParams used to create
213
+ * the project. The REST response only includes a subset of generation
214
+ * settings; pass the original params if you persisted them (e.g., in MMKV)
215
+ * so the returned snapshot's `params` field is fully populated.
216
+ */
217
+ async fetch(projectId: string, paramsOverride?: ProjectParams): Promise<Project> {
218
+ const id = projectId.toUpperCase();
219
+ const res = await this._client.rest.get<ApiResponse<RawProject>>(`/v1/projects/${id}`);
220
+ const project = rawProjectToProject(res.data, paramsOverride);
221
+ this._projects.set(project.id, project);
222
+ this._notifyChanged(project);
223
+ return project;
224
+ }
225
+
226
+ /** Subscribe to changes for a specific project. Returns an unsubscribe function. */
227
+ subscribe(projectId: string, listener: (project: Project) => void): () => void {
228
+ const id = projectId.toUpperCase();
229
+ let set = this._subscribers.get(id);
230
+ if (!set) {
231
+ set = new Set();
232
+ this._subscribers.set(id, set);
233
+ }
234
+ set.add(listener);
235
+ return () => {
236
+ const current = this._subscribers.get(id);
237
+ if (!current) return;
238
+ current.delete(listener);
239
+ if (current.size === 0) this._subscribers.delete(id);
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Resolve when the project reaches a terminal state. On success, returns the
245
+ * downloadable result URLs (fetched from the server). On failure or cancel,
246
+ * rejects with an Error describing the reason.
247
+ */
248
+ async waitForCompletion(projectId: string): Promise<string[]> {
249
+ const id = projectId.toUpperCase();
250
+ const project = this._projects.get(id);
251
+ if (!project) {
252
+ throw new Error(`Project ${projectId} is not known to this client`);
253
+ }
254
+ if (project.status === 'completed') {
255
+ return project.resultUrls.length ? project.resultUrls : this._fetchResultUrls(project);
256
+ }
257
+ if (project.status === 'failed' || project.status === 'canceled') {
258
+ throw new Error(project.error?.message || `Project ${project.status}`);
259
+ }
260
+ return new Promise<string[]>((resolve, reject) => {
261
+ const unsubscribe = this.subscribe(id, (p) => {
262
+ if (p.status === 'completed') {
263
+ unsubscribe();
264
+ if (p.resultUrls.length) resolve(p.resultUrls);
265
+ else this._fetchResultUrls(p).then(resolve).catch(reject);
266
+ } else if (p.status === 'failed' || p.status === 'canceled') {
267
+ unsubscribe();
268
+ reject(new Error(p.error?.message || `Project ${p.status}`));
269
+ }
270
+ });
271
+ });
272
+ }
273
+
274
+ /** Request a download URL for a specific job's result image. */
275
+ async getResultUrl(projectId: string, jobId: string): Promise<string> {
276
+ const res = await this._client.socketRest.get<ApiResponse<string | { url: string }>>(
277
+ `/v1/image/downloadUrl`,
278
+ { projectId: projectId.toUpperCase(), jobId: jobId.toUpperCase(), type: 'complete' }
279
+ );
280
+ const d: any = res.data;
281
+ return typeof d === 'string' ? d : d.url;
282
+ }
283
+
284
+ /**
285
+ * Request cancellation of a project. Server confirms via
286
+ * `artistCancelConfirmation`; the project's status flips to 'canceled' if
287
+ * accepted, otherwise stays in its current state.
288
+ */
289
+ async cancel(projectId: string): Promise<void> {
290
+ const id = projectId.toUpperCase();
291
+ await this._client.socket.send('jobError', {
292
+ jobID: id,
293
+ error: 'artistCanceled',
294
+ error_message: 'Artist Cancelled Job',
295
+ isFromWorker: false
296
+ });
297
+ }
298
+
299
+ // ===========================================================================
300
+ // Internal: event wiring
301
+ // ===========================================================================
302
+
303
+ private _wireSocketHandlers() {
304
+ const s = this._client.socket;
305
+ s.on('jobStarted', (e) => this._onJobStarted(e));
306
+ s.on('jobInitiatingModel', (e) => this._onJobInitiatingModel(e));
307
+ s.on('jobLifecycleCompleted', (e) => this._onJobLifecycleCompleted(e));
308
+ s.on('jobProgress', (e) => this._onJobProgress(e));
309
+ s.on('jobResult', (e) => this._onJobResult(e));
310
+ s.on('jobError', (e) => this._onJobError(e));
311
+ s.on('jobETA', (e) => this._onJobETA(e));
312
+ s.on('artistCancelConfirmation', (e) => this._onCancelConfirmation(e));
313
+ }
314
+
315
+ private _onJobStarted(e: JobStartedEvent) {
316
+ const project = this._projects.get(e.projectId);
317
+ if (!project) return;
318
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
319
+ ...job,
320
+ status: 'processing',
321
+ workerName: e.workerName
322
+ }));
323
+ this._setStatusIfPending(next, 'processing');
324
+ this._notifyChanged(next);
325
+ }
326
+
327
+ private _onJobInitiatingModel(e: JobInitiatingModelEvent) {
328
+ const project = this._projects.get(e.projectId);
329
+ if (!project) return;
330
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
331
+ ...job,
332
+ status: 'initiatingModel'
333
+ }));
334
+ this._setStatusIfPending(next, 'processing');
335
+ this._notifyChanged(next);
336
+ }
337
+
338
+ private _onJobLifecycleCompleted(e: JobLifecycleCompletedEvent) {
339
+ // jobState.type === 'jobCompleted' is the lifecycle marker from server.
340
+ // The real result data arrives via jobResult; we just ensure the job is
341
+ // marked completed here if it wasn't already.
342
+ const project = this._projects.get(e.projectId);
343
+ if (!project) return;
344
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
345
+ ...job,
346
+ status: job.status === 'failed' ? 'failed' : 'completed',
347
+ progress: 1
348
+ }));
349
+ this._maybeFinalizeProject(next);
350
+ }
351
+
352
+ private _onJobProgress(e: JobProgressEvent) {
353
+ const project = this._projects.get(e.projectId);
354
+ if (!project) return;
355
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
356
+ ...job,
357
+ status: 'processing',
358
+ progress: e.progress,
359
+ step: e.step,
360
+ stepCount: e.stepCount
361
+ }));
362
+ this._setStatusIfPending(next, 'processing');
363
+ next.progress = this._computeOverallProgress(next.jobs);
364
+ this._notifyChanged(next);
365
+ this.emit('progress', { project: next, jobId: e.jobId });
366
+ }
367
+
368
+ private _onJobResult(e: JobResultEvent) {
369
+ const project = this._projects.get(e.projectId);
370
+ if (!project) return;
371
+ if (e.userCanceled) {
372
+ const error: ErrorData = {
373
+ code: 5004,
374
+ originalCode: 'artistCanceled',
375
+ message: 'Artist cancelled the job'
376
+ };
377
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
378
+ ...job,
379
+ status: 'canceled',
380
+ error
381
+ }));
382
+ this._maybeFinalizeProject(next);
383
+ return;
384
+ }
385
+ if (e.triggeredNSFWFilter) {
386
+ const error: ErrorData = {
387
+ code: 5006,
388
+ originalCode: 'sensitiveContent',
389
+ message: 'NSFW filter triggered'
390
+ };
391
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
392
+ ...job,
393
+ status: 'failed',
394
+ isNSFW: true,
395
+ seed: e.seed,
396
+ error
397
+ }));
398
+ this._maybeFinalizeProject(next);
399
+ return;
400
+ }
401
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
402
+ ...job,
403
+ status: 'completed',
404
+ progress: 1,
405
+ seed: e.seed
406
+ }));
407
+ this._maybeFinalizeProject(next);
408
+ }
409
+
410
+ private _onJobError(e: JobErrorEvent) {
411
+ const project = this._projects.get(e.projectId);
412
+ if (!project) return;
413
+ const error: ErrorData = {
414
+ code: 5000,
415
+ originalCode: e.rawCode,
416
+ message: e.message
417
+ };
418
+ if (e.jobId) {
419
+ const next = this._withJobMutation(project, e.jobId, (job) => ({
420
+ ...job,
421
+ status: 'failed',
422
+ error
423
+ }));
424
+ this._maybeFinalizeProject(next);
425
+ return;
426
+ }
427
+ // Project-level error
428
+ const next: Project = {
429
+ ...project,
430
+ status: 'failed',
431
+ error,
432
+ updatedAt: Date.now()
433
+ };
434
+ this._projects.set(project.id, next);
435
+ this._notifyChanged(next);
436
+ this.emit('failed', next);
437
+ this._scheduleGC(next.id);
438
+ }
439
+
440
+ private _onJobETA(e: JobETAEvent) {
441
+ const project = this._projects.get(e.projectId);
442
+ if (!project) return;
443
+ const next: Project = { ...project, eta: e.etaSeconds, updatedAt: Date.now() };
444
+ this._projects.set(project.id, next);
445
+ this._notifyChanged(next);
446
+ }
447
+
448
+ private _onCancelConfirmation(e: ArtistCancelConfirmationEvent) {
449
+ const project = this._projects.get(e.projectId);
450
+ if (!project) return;
451
+ if (!e.didCancel) {
452
+ this._client.logger.warn(
453
+ `Cancel request for ${e.projectId} was rejected: ${e.errorMessage || 'unknown reason'}`
454
+ );
455
+ return;
456
+ }
457
+ const next: Project = {
458
+ ...project,
459
+ status: 'canceled',
460
+ error: { code: 5004, originalCode: 'artistCanceled', message: 'Artist canceled project' },
461
+ updatedAt: Date.now()
462
+ };
463
+ this._projects.set(project.id, next);
464
+ this._notifyChanged(next);
465
+ this.emit('failed', next);
466
+ this._scheduleGC(next.id);
467
+ }
468
+
469
+ // ===========================================================================
470
+ // Internal: state machinery
471
+ // ===========================================================================
472
+
473
+ private _withJobMutation(
474
+ project: Project,
475
+ jobId: string | null,
476
+ mutate: (job: Job) => Job
477
+ ): Project {
478
+ if (!jobId) {
479
+ // Some events (jobStarted before imgID is assigned) carry no jobId. We
480
+ // don't synthesize a job in that case; just bump the updatedAt.
481
+ const next = { ...project, updatedAt: Date.now() };
482
+ this._projects.set(project.id, next);
483
+ return next;
484
+ }
485
+ const existingIdx = project.jobs.findIndex((j) => j.id === jobId);
486
+ const existing: Job =
487
+ existingIdx >= 0 ? project.jobs[existingIdx]! : this._emptyJob(project.id, jobId);
488
+ const updated = mutate(existing);
489
+ const jobs =
490
+ existingIdx >= 0
491
+ ? project.jobs.map((j, i) => (i === existingIdx ? updated : j))
492
+ : [...project.jobs, updated];
493
+ const next: Project = { ...project, jobs, updatedAt: Date.now() };
494
+ this._projects.set(project.id, next);
495
+ return next;
496
+ }
497
+
498
+ private _emptyJob(projectId: string, jobId: string): Job {
499
+ return {
500
+ id: jobId,
501
+ projectId,
502
+ status: 'pending',
503
+ progress: 0,
504
+ step: 0,
505
+ stepCount: 0,
506
+ seed: null,
507
+ resultUrl: null,
508
+ previewUrl: null,
509
+ isNSFW: false,
510
+ workerName: null,
511
+ error: null
512
+ };
513
+ }
514
+
515
+ private _setStatusIfPending(project: Project, status: ProjectStatus) {
516
+ if (project.status !== 'pending') return;
517
+ const next: Project = { ...project, status, updatedAt: Date.now() };
518
+ this._projects.set(project.id, next);
519
+ }
520
+
521
+ private _computeOverallProgress(jobs: Job[]): number {
522
+ if (!jobs.length) return 0;
523
+ const sum = jobs.reduce((acc, j) => acc + (j.progress || 0), 0);
524
+ return sum / jobs.length;
525
+ }
526
+
527
+ private _maybeFinalizeProject(project: Project) {
528
+ const expected = Math.max(1, project.params.numberOfMedia);
529
+ const knownJobs = project.jobs.length;
530
+ const completedJobs = project.jobs.filter((j) => j.status === 'completed').length;
531
+ const allKnownTerminal =
532
+ knownJobs > 0 && project.jobs.every((j) => TERMINAL_JOB_STATUSES.includes(j.status));
533
+
534
+ if (knownJobs >= expected && allKnownTerminal) {
535
+ const anyCompleted = completedJobs > 0;
536
+ const next: Project = {
537
+ ...project,
538
+ status: anyCompleted ? 'completed' : 'failed',
539
+ progress: this._computeOverallProgress(project.jobs),
540
+ error: anyCompleted
541
+ ? null
542
+ : (project.jobs.find((j) => j.error)?.error ?? {
543
+ code: 5000,
544
+ message: 'All jobs failed'
545
+ }),
546
+ updatedAt: Date.now()
547
+ };
548
+ this._projects.set(project.id, next);
549
+ this._notifyChanged(next);
550
+ if (next.status === 'completed') {
551
+ this.emit('completed', next);
552
+ } else {
553
+ this.emit('failed', next);
554
+ }
555
+ this._scheduleGC(next.id);
556
+ return;
557
+ }
558
+
559
+ // Not terminal yet — just notify of the in-progress change.
560
+ const nextProgress = this._computeOverallProgress(project.jobs);
561
+ const next: Project = { ...project, progress: nextProgress, updatedAt: Date.now() };
562
+ this._projects.set(project.id, next);
563
+ this._notifyChanged(next);
564
+ }
565
+
566
+ private async _fetchResultUrls(project: Project): Promise<string[]> {
567
+ const urls: string[] = [];
568
+ for (const job of project.jobs) {
569
+ if (job.status !== 'completed') continue;
570
+ try {
571
+ const url = await this.getResultUrl(project.id, job.id);
572
+ urls.push(url);
573
+ // Update cached snapshot with the resolved URL
574
+ const cached = this._projects.get(project.id);
575
+ if (cached) {
576
+ const jobs = cached.jobs.map((j) => (j.id === job.id ? { ...j, resultUrl: url } : j));
577
+ const next: Project = { ...cached, jobs, resultUrls: urls.slice() };
578
+ this._projects.set(project.id, next);
579
+ this._notifyChanged(next);
580
+ }
581
+ } catch (e) {
582
+ this._client.logger.warn(`Failed to fetch result URL for job ${job.id}:`, e);
583
+ }
584
+ }
585
+ return urls;
586
+ }
587
+
588
+ private _notifyChanged(project: Project) {
589
+ const subs = this._subscribers.get(project.id);
590
+ if (subs) {
591
+ for (const fn of subs) fn(project);
592
+ }
593
+ this.emit('changed', project);
594
+ }
595
+
596
+ private _scheduleGC(projectId: string) {
597
+ const existing = this._gcTimers.get(projectId);
598
+ if (existing) clearTimeout(existing);
599
+ const timer = setTimeout(() => {
600
+ this._projects.delete(projectId);
601
+ this._subscribers.delete(projectId);
602
+ this._gcTimers.delete(projectId);
603
+ }, GC_DELAY_MS);
604
+ this._gcTimers.set(projectId, timer);
605
+ }
606
+ }
607
+
608
+ export default ProjectsApi;