@juspay/neurolink 9.64.0 → 9.65.0

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 (322) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +18 -17
  3. package/dist/adapters/providerImageAdapter.js +29 -1
  4. package/dist/adapters/replicate/auth.d.ts +19 -0
  5. package/dist/adapters/replicate/auth.js +32 -0
  6. package/dist/adapters/replicate/predictionLifecycle.d.ts +46 -0
  7. package/dist/adapters/replicate/predictionLifecycle.js +283 -0
  8. package/dist/adapters/video/klingVideoHandler.d.ts +37 -0
  9. package/dist/adapters/video/klingVideoHandler.js +305 -0
  10. package/dist/adapters/video/replicateVideoHandler.d.ts +29 -0
  11. package/dist/adapters/video/replicateVideoHandler.js +157 -0
  12. package/dist/adapters/video/runwayVideoHandler.d.ts +32 -0
  13. package/dist/adapters/video/runwayVideoHandler.js +316 -0
  14. package/dist/adapters/video/vertexVideoHandler.d.ts +19 -1
  15. package/dist/adapters/video/vertexVideoHandler.js +33 -9
  16. package/dist/autoresearch/runner.js +8 -2
  17. package/dist/avatar/index.d.ts +13 -0
  18. package/dist/avatar/index.js +13 -0
  19. package/dist/avatar/providers/DIDAvatar.d.ts +49 -0
  20. package/dist/avatar/providers/DIDAvatar.js +501 -0
  21. package/dist/avatar/providers/HeyGenAvatar.d.ts +30 -0
  22. package/dist/avatar/providers/HeyGenAvatar.js +337 -0
  23. package/dist/avatar/providers/ReplicateAvatar.d.ts +36 -0
  24. package/dist/avatar/providers/ReplicateAvatar.js +267 -0
  25. package/dist/browser/neurolink.min.js +633 -610
  26. package/dist/cli/commands/mcp.js +29 -0
  27. package/dist/cli/commands/proxy.js +24 -5
  28. package/dist/cli/factories/commandFactory.d.ts +11 -1
  29. package/dist/cli/factories/commandFactory.js +291 -38
  30. package/dist/constants/contextWindows.js +101 -0
  31. package/dist/constants/enums.d.ts +273 -2
  32. package/dist/constants/enums.js +290 -1
  33. package/dist/constants/videoErrors.d.ts +4 -0
  34. package/dist/constants/videoErrors.js +4 -0
  35. package/dist/core/baseProvider.d.ts +22 -2
  36. package/dist/core/baseProvider.js +217 -11
  37. package/dist/core/constants.d.ts +11 -0
  38. package/dist/core/constants.js +69 -1
  39. package/dist/core/redisConversationMemoryManager.js +6 -0
  40. package/dist/evaluation/index.d.ts +2 -0
  41. package/dist/evaluation/index.js +4 -0
  42. package/dist/factories/providerFactory.js +7 -1
  43. package/dist/factories/providerRegistry.js +202 -5
  44. package/dist/features/ppt/contentPlanner.js +42 -14
  45. package/dist/index.d.ts +9 -1
  46. package/dist/index.js +16 -1
  47. package/dist/lib/adapters/providerImageAdapter.js +29 -1
  48. package/dist/lib/adapters/replicate/auth.d.ts +19 -0
  49. package/dist/lib/adapters/replicate/auth.js +33 -0
  50. package/dist/lib/adapters/replicate/predictionLifecycle.d.ts +46 -0
  51. package/dist/lib/adapters/replicate/predictionLifecycle.js +284 -0
  52. package/dist/lib/adapters/video/klingVideoHandler.d.ts +37 -0
  53. package/dist/lib/adapters/video/klingVideoHandler.js +306 -0
  54. package/dist/lib/adapters/video/replicateVideoHandler.d.ts +29 -0
  55. package/dist/lib/adapters/video/replicateVideoHandler.js +158 -0
  56. package/dist/lib/adapters/video/runwayVideoHandler.d.ts +32 -0
  57. package/dist/lib/adapters/video/runwayVideoHandler.js +317 -0
  58. package/dist/lib/adapters/video/vertexVideoHandler.d.ts +19 -1
  59. package/dist/lib/adapters/video/vertexVideoHandler.js +33 -9
  60. package/dist/lib/autoresearch/runner.js +8 -2
  61. package/dist/lib/avatar/index.d.ts +13 -0
  62. package/dist/lib/avatar/index.js +14 -0
  63. package/dist/lib/avatar/providers/DIDAvatar.d.ts +49 -0
  64. package/dist/lib/avatar/providers/DIDAvatar.js +502 -0
  65. package/dist/lib/avatar/providers/HeyGenAvatar.d.ts +30 -0
  66. package/dist/lib/avatar/providers/HeyGenAvatar.js +338 -0
  67. package/dist/lib/avatar/providers/ReplicateAvatar.d.ts +36 -0
  68. package/dist/lib/avatar/providers/ReplicateAvatar.js +268 -0
  69. package/dist/lib/constants/contextWindows.js +101 -0
  70. package/dist/lib/constants/enums.d.ts +273 -2
  71. package/dist/lib/constants/enums.js +290 -1
  72. package/dist/lib/constants/videoErrors.d.ts +4 -0
  73. package/dist/lib/constants/videoErrors.js +4 -0
  74. package/dist/lib/core/baseProvider.d.ts +22 -2
  75. package/dist/lib/core/baseProvider.js +217 -11
  76. package/dist/lib/core/constants.d.ts +11 -0
  77. package/dist/lib/core/constants.js +69 -1
  78. package/dist/lib/core/redisConversationMemoryManager.js +6 -0
  79. package/dist/lib/evaluation/index.d.ts +2 -0
  80. package/dist/lib/evaluation/index.js +4 -0
  81. package/dist/lib/factories/providerFactory.js +7 -1
  82. package/dist/lib/factories/providerRegistry.js +202 -5
  83. package/dist/lib/features/ppt/contentPlanner.js +42 -14
  84. package/dist/lib/index.d.ts +9 -1
  85. package/dist/lib/index.js +16 -1
  86. package/dist/lib/middleware/builtin/lifecycle.js +39 -9
  87. package/dist/lib/music/index.d.ts +13 -0
  88. package/dist/lib/music/index.js +14 -0
  89. package/dist/lib/music/providers/BeatovenMusic.d.ts +31 -0
  90. package/dist/lib/music/providers/BeatovenMusic.js +334 -0
  91. package/dist/lib/music/providers/ElevenLabsMusic.d.ts +30 -0
  92. package/dist/lib/music/providers/ElevenLabsMusic.js +169 -0
  93. package/dist/lib/music/providers/LyriaMusic.d.ts +29 -0
  94. package/dist/lib/music/providers/LyriaMusic.js +173 -0
  95. package/dist/lib/music/providers/ReplicateMusic.d.ts +31 -0
  96. package/dist/lib/music/providers/ReplicateMusic.js +262 -0
  97. package/dist/lib/neurolink.d.ts +30 -0
  98. package/dist/lib/neurolink.js +323 -77
  99. package/dist/lib/providers/amazonBedrock.d.ts +10 -0
  100. package/dist/lib/providers/amazonBedrock.js +94 -39
  101. package/dist/lib/providers/anthropic.js +55 -7
  102. package/dist/lib/providers/anthropicBaseProvider.js +1 -1
  103. package/dist/lib/providers/azureOpenai.js +66 -17
  104. package/dist/lib/providers/cloudflare.d.ts +35 -0
  105. package/dist/lib/providers/cloudflare.js +174 -0
  106. package/dist/lib/providers/cohere.d.ts +52 -0
  107. package/dist/lib/providers/cohere.js +253 -0
  108. package/dist/lib/providers/deepseek.js +72 -17
  109. package/dist/lib/providers/fireworks.d.ts +33 -0
  110. package/dist/lib/providers/fireworks.js +164 -0
  111. package/dist/lib/providers/googleAiStudio.js +45 -6
  112. package/dist/lib/providers/googleNativeGemini3.d.ts +24 -1
  113. package/dist/lib/providers/googleNativeGemini3.js +173 -21
  114. package/dist/lib/providers/googleVertex.js +173 -17
  115. package/dist/lib/providers/groq.d.ts +33 -0
  116. package/dist/lib/providers/groq.js +181 -0
  117. package/dist/lib/providers/huggingFace.js +9 -8
  118. package/dist/lib/providers/ideogram.d.ts +34 -0
  119. package/dist/lib/providers/ideogram.js +184 -0
  120. package/dist/lib/providers/index.d.ts +13 -0
  121. package/dist/lib/providers/index.js +13 -0
  122. package/dist/lib/providers/jina.d.ts +59 -0
  123. package/dist/lib/providers/jina.js +218 -0
  124. package/dist/lib/providers/llamaCpp.js +14 -46
  125. package/dist/lib/providers/lmStudio.js +14 -47
  126. package/dist/lib/providers/mistral.js +7 -7
  127. package/dist/lib/providers/nvidiaNim.js +160 -19
  128. package/dist/lib/providers/ollama.js +7 -7
  129. package/dist/lib/providers/openAI.d.ts +22 -1
  130. package/dist/lib/providers/openAI.js +181 -0
  131. package/dist/lib/providers/openRouter.js +35 -23
  132. package/dist/lib/providers/openaiCompatible.js +9 -8
  133. package/dist/lib/providers/perplexity.d.ts +33 -0
  134. package/dist/lib/providers/perplexity.js +179 -0
  135. package/dist/lib/providers/recraft.d.ts +34 -0
  136. package/dist/lib/providers/recraft.js +197 -0
  137. package/dist/lib/providers/replicate.d.ts +75 -0
  138. package/dist/lib/providers/replicate.js +403 -0
  139. package/dist/lib/providers/stability.d.ts +37 -0
  140. package/dist/lib/providers/stability.js +191 -0
  141. package/dist/lib/providers/togetherAi.d.ts +33 -0
  142. package/dist/lib/providers/togetherAi.js +176 -0
  143. package/dist/lib/providers/voyage.d.ts +47 -0
  144. package/dist/lib/providers/voyage.js +177 -0
  145. package/dist/lib/providers/xai.d.ts +33 -0
  146. package/dist/lib/providers/xai.js +172 -0
  147. package/dist/lib/telemetry/index.d.ts +1 -1
  148. package/dist/lib/telemetry/index.js +1 -1
  149. package/dist/lib/telemetry/tracers.d.ts +19 -0
  150. package/dist/lib/telemetry/tracers.js +19 -0
  151. package/dist/lib/telemetry/withSpan.d.ts +35 -0
  152. package/dist/lib/telemetry/withSpan.js +103 -0
  153. package/dist/lib/types/avatar.d.ts +143 -0
  154. package/dist/lib/types/avatar.js +20 -0
  155. package/dist/lib/types/cli.d.ts +6 -0
  156. package/dist/lib/types/generate.d.ts +62 -5
  157. package/dist/lib/types/index.d.ts +5 -0
  158. package/dist/lib/types/index.js +7 -0
  159. package/dist/lib/types/middleware.d.ts +27 -0
  160. package/dist/lib/types/multimodal.d.ts +35 -2
  161. package/dist/lib/types/music.d.ts +165 -0
  162. package/dist/lib/types/music.js +21 -0
  163. package/dist/lib/types/providers.d.ts +144 -1
  164. package/dist/lib/types/replicate.d.ts +67 -0
  165. package/dist/lib/types/replicate.js +10 -0
  166. package/dist/lib/types/safeFetch.d.ts +15 -0
  167. package/dist/lib/types/safeFetch.js +7 -0
  168. package/dist/lib/types/stream.d.ts +2 -1
  169. package/dist/lib/types/tools.d.ts +13 -0
  170. package/dist/lib/types/video.d.ts +89 -0
  171. package/dist/lib/types/video.js +15 -0
  172. package/dist/lib/utils/avatarProcessor.d.ts +68 -0
  173. package/dist/lib/utils/avatarProcessor.js +172 -0
  174. package/dist/lib/utils/cloneOptions.d.ts +36 -0
  175. package/dist/lib/utils/cloneOptions.js +62 -0
  176. package/dist/lib/utils/lifecycleCallbacks.d.ts +51 -8
  177. package/dist/lib/utils/lifecycleCallbacks.js +82 -26
  178. package/dist/lib/utils/lifecycleTimeout.d.ts +25 -0
  179. package/dist/lib/utils/lifecycleTimeout.js +39 -0
  180. package/dist/lib/utils/logSanitize.d.ts +49 -0
  181. package/dist/lib/utils/logSanitize.js +170 -0
  182. package/dist/lib/utils/loggingFetch.d.ts +29 -0
  183. package/dist/lib/utils/loggingFetch.js +60 -0
  184. package/dist/lib/utils/messageBuilder.js +43 -25
  185. package/dist/lib/utils/modelChoices.js +236 -3
  186. package/dist/lib/utils/musicProcessor.d.ts +67 -0
  187. package/dist/lib/utils/musicProcessor.js +189 -0
  188. package/dist/lib/utils/optionsConversion.js +3 -2
  189. package/dist/lib/utils/parameterValidation.js +14 -4
  190. package/dist/lib/utils/pricing.js +193 -0
  191. package/dist/lib/utils/providerConfig.d.ts +55 -0
  192. package/dist/lib/utils/providerConfig.js +224 -0
  193. package/dist/lib/utils/safeFetch.d.ts +26 -0
  194. package/dist/lib/utils/safeFetch.js +83 -0
  195. package/dist/lib/utils/sizeGuard.d.ts +34 -0
  196. package/dist/lib/utils/sizeGuard.js +45 -0
  197. package/dist/lib/utils/ssrfGuard.d.ts +52 -0
  198. package/dist/lib/utils/ssrfGuard.js +411 -0
  199. package/dist/lib/utils/videoProcessor.d.ts +60 -0
  200. package/dist/lib/utils/videoProcessor.js +201 -0
  201. package/dist/lib/voice/providers/FishAudioTTS.d.ts +27 -0
  202. package/dist/lib/voice/providers/FishAudioTTS.js +183 -0
  203. package/dist/lib/workflow/core/ensembleExecutor.js +26 -9
  204. package/dist/middleware/builtin/lifecycle.js +39 -9
  205. package/dist/music/index.d.ts +13 -0
  206. package/dist/music/index.js +13 -0
  207. package/dist/music/providers/BeatovenMusic.d.ts +31 -0
  208. package/dist/music/providers/BeatovenMusic.js +333 -0
  209. package/dist/music/providers/ElevenLabsMusic.d.ts +30 -0
  210. package/dist/music/providers/ElevenLabsMusic.js +168 -0
  211. package/dist/music/providers/LyriaMusic.d.ts +29 -0
  212. package/dist/music/providers/LyriaMusic.js +172 -0
  213. package/dist/music/providers/ReplicateMusic.d.ts +31 -0
  214. package/dist/music/providers/ReplicateMusic.js +261 -0
  215. package/dist/neurolink.d.ts +30 -0
  216. package/dist/neurolink.js +323 -77
  217. package/dist/providers/amazonBedrock.d.ts +10 -0
  218. package/dist/providers/amazonBedrock.js +94 -39
  219. package/dist/providers/anthropic.js +55 -7
  220. package/dist/providers/anthropicBaseProvider.js +1 -1
  221. package/dist/providers/azureOpenai.js +66 -17
  222. package/dist/providers/cloudflare.d.ts +35 -0
  223. package/dist/providers/cloudflare.js +173 -0
  224. package/dist/providers/cohere.d.ts +52 -0
  225. package/dist/providers/cohere.js +252 -0
  226. package/dist/providers/deepseek.js +72 -17
  227. package/dist/providers/fireworks.d.ts +33 -0
  228. package/dist/providers/fireworks.js +163 -0
  229. package/dist/providers/googleAiStudio.js +45 -6
  230. package/dist/providers/googleNativeGemini3.d.ts +24 -1
  231. package/dist/providers/googleNativeGemini3.js +173 -21
  232. package/dist/providers/googleVertex.js +173 -17
  233. package/dist/providers/groq.d.ts +33 -0
  234. package/dist/providers/groq.js +180 -0
  235. package/dist/providers/huggingFace.js +9 -8
  236. package/dist/providers/ideogram.d.ts +34 -0
  237. package/dist/providers/ideogram.js +183 -0
  238. package/dist/providers/index.d.ts +13 -0
  239. package/dist/providers/index.js +13 -0
  240. package/dist/providers/jina.d.ts +59 -0
  241. package/dist/providers/jina.js +217 -0
  242. package/dist/providers/llamaCpp.js +14 -46
  243. package/dist/providers/lmStudio.js +14 -47
  244. package/dist/providers/mistral.js +7 -7
  245. package/dist/providers/nvidiaNim.js +160 -19
  246. package/dist/providers/ollama.js +7 -7
  247. package/dist/providers/openAI.d.ts +22 -1
  248. package/dist/providers/openAI.js +181 -0
  249. package/dist/providers/openRouter.js +35 -23
  250. package/dist/providers/openaiCompatible.js +9 -8
  251. package/dist/providers/perplexity.d.ts +33 -0
  252. package/dist/providers/perplexity.js +178 -0
  253. package/dist/providers/recraft.d.ts +34 -0
  254. package/dist/providers/recraft.js +196 -0
  255. package/dist/providers/replicate.d.ts +75 -0
  256. package/dist/providers/replicate.js +402 -0
  257. package/dist/providers/stability.d.ts +37 -0
  258. package/dist/providers/stability.js +190 -0
  259. package/dist/providers/togetherAi.d.ts +33 -0
  260. package/dist/providers/togetherAi.js +175 -0
  261. package/dist/providers/voyage.d.ts +47 -0
  262. package/dist/providers/voyage.js +176 -0
  263. package/dist/providers/xai.d.ts +33 -0
  264. package/dist/providers/xai.js +171 -0
  265. package/dist/telemetry/index.d.ts +1 -1
  266. package/dist/telemetry/index.js +1 -1
  267. package/dist/telemetry/tracers.d.ts +19 -0
  268. package/dist/telemetry/tracers.js +19 -0
  269. package/dist/telemetry/withSpan.d.ts +35 -0
  270. package/dist/telemetry/withSpan.js +103 -0
  271. package/dist/types/avatar.d.ts +143 -0
  272. package/dist/types/avatar.js +19 -0
  273. package/dist/types/cli.d.ts +6 -0
  274. package/dist/types/generate.d.ts +62 -5
  275. package/dist/types/index.d.ts +5 -0
  276. package/dist/types/index.js +7 -0
  277. package/dist/types/middleware.d.ts +27 -0
  278. package/dist/types/multimodal.d.ts +35 -2
  279. package/dist/types/music.d.ts +165 -0
  280. package/dist/types/music.js +20 -0
  281. package/dist/types/providers.d.ts +144 -1
  282. package/dist/types/replicate.d.ts +67 -0
  283. package/dist/types/replicate.js +9 -0
  284. package/dist/types/safeFetch.d.ts +15 -0
  285. package/dist/types/safeFetch.js +6 -0
  286. package/dist/types/stream.d.ts +2 -1
  287. package/dist/types/tools.d.ts +13 -0
  288. package/dist/types/video.d.ts +89 -0
  289. package/dist/types/video.js +14 -0
  290. package/dist/utils/avatarProcessor.d.ts +68 -0
  291. package/dist/utils/avatarProcessor.js +171 -0
  292. package/dist/utils/cloneOptions.d.ts +36 -0
  293. package/dist/utils/cloneOptions.js +61 -0
  294. package/dist/utils/lifecycleCallbacks.d.ts +51 -8
  295. package/dist/utils/lifecycleCallbacks.js +82 -26
  296. package/dist/utils/lifecycleTimeout.d.ts +25 -0
  297. package/dist/utils/lifecycleTimeout.js +38 -0
  298. package/dist/utils/logSanitize.d.ts +49 -0
  299. package/dist/utils/logSanitize.js +169 -0
  300. package/dist/utils/loggingFetch.d.ts +29 -0
  301. package/dist/utils/loggingFetch.js +59 -0
  302. package/dist/utils/messageBuilder.js +43 -25
  303. package/dist/utils/modelChoices.js +236 -3
  304. package/dist/utils/musicProcessor.d.ts +67 -0
  305. package/dist/utils/musicProcessor.js +188 -0
  306. package/dist/utils/optionsConversion.js +3 -2
  307. package/dist/utils/parameterValidation.js +14 -4
  308. package/dist/utils/pricing.js +193 -0
  309. package/dist/utils/providerConfig.d.ts +55 -0
  310. package/dist/utils/providerConfig.js +224 -0
  311. package/dist/utils/safeFetch.d.ts +26 -0
  312. package/dist/utils/safeFetch.js +82 -0
  313. package/dist/utils/sizeGuard.d.ts +34 -0
  314. package/dist/utils/sizeGuard.js +44 -0
  315. package/dist/utils/ssrfGuard.d.ts +52 -0
  316. package/dist/utils/ssrfGuard.js +410 -0
  317. package/dist/utils/videoProcessor.d.ts +60 -0
  318. package/dist/utils/videoProcessor.js +200 -0
  319. package/dist/voice/providers/FishAudioTTS.d.ts +27 -0
  320. package/dist/voice/providers/FishAudioTTS.js +182 -0
  321. package/dist/workflow/core/ensembleExecutor.js +26 -9
  322. package/package.json +32 -5
@@ -0,0 +1,337 @@
1
+ /**
2
+ * HeyGen Avatar Handler
3
+ *
4
+ * Async talking-head generation. Submits a video.generate request, polls
5
+ * the video status, downloads the result MP4.
6
+ *
7
+ * @module avatar/providers/HeyGenAvatar
8
+ * @see https://docs.heygen.com/reference/avatar-video
9
+ */
10
+ import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
11
+ import { AVATAR_ERROR_CODES, AvatarError, } from "../../utils/avatarProcessor.js";
12
+ import { logger } from "../../utils/logger.js";
13
+ import { sanitizeForLog } from "../../utils/logSanitize.js";
14
+ import { safeDownload } from "../../utils/safeFetch.js";
15
+ import { MAX_VIDEO_BYTES } from "../../utils/sizeGuard.js";
16
+ const DEFAULT_BASE_URL = "https://api.heygen.com/v2";
17
+ const REQUEST_TIMEOUT_MS = 30_000;
18
+ const POLL_INTERVAL_MS = 5_000;
19
+ const TOTAL_TIMEOUT_MS = 5 * 60_000;
20
+ /**
21
+ * HeyGen Avatar Handler.
22
+ *
23
+ * Auth: `X-API-Key: ${HEYGEN_API_KEY}`. The HeyGen API expects an
24
+ * `avatar_id` (HeyGen's own avatar catalog) — pass it via `options.voice`
25
+ * for legacy callers, or `options.avatarId` for explicit users.
26
+ */
27
+ export class HeyGenAvatar {
28
+ maxAudioDurationSeconds = 300; // 5 minutes
29
+ supportedFormats = ["mp4"];
30
+ apiKey;
31
+ baseUrl;
32
+ constructor(apiKey) {
33
+ const resolved = (apiKey ?? process.env.HEYGEN_API_KEY ?? "").trim();
34
+ this.apiKey = resolved.length > 0 ? resolved : null;
35
+ this.baseUrl = (process.env.HEYGEN_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
36
+ }
37
+ isConfigured() {
38
+ return this.apiKey !== null;
39
+ }
40
+ async generate(options) {
41
+ if (!this.apiKey) {
42
+ throw new AvatarError({
43
+ code: AVATAR_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
44
+ message: "HEYGEN_API_KEY not configured",
45
+ category: ErrorCategory.CONFIGURATION,
46
+ severity: ErrorSeverity.HIGH,
47
+ retriable: false,
48
+ });
49
+ }
50
+ if (!options.text && !options.audio) {
51
+ throw new AvatarError({
52
+ code: AVATAR_ERROR_CODES.AUDIO_REQUIRED,
53
+ message: "HeyGen requires either `text` or `audio` to drive the avatar",
54
+ category: ErrorCategory.VALIDATION,
55
+ severity: ErrorSeverity.MEDIUM,
56
+ retriable: false,
57
+ });
58
+ }
59
+ const startTime = Date.now();
60
+ const abortSignal = options.abortSignal;
61
+ const videoId = await this.submitVideo(options);
62
+ const videoUrl = await this.pollUntilComplete(videoId, abortSignal);
63
+ const buffer = await this.download(videoUrl);
64
+ const latency = Date.now() - startTime;
65
+ logger.info(`[HeyGenAvatar] Generated ${buffer.length} bytes in ${latency}ms — video ${videoId}`);
66
+ return {
67
+ buffer,
68
+ format: "mp4",
69
+ size: buffer.length,
70
+ provider: "heygen",
71
+ metadata: {
72
+ latency,
73
+ provider: "heygen",
74
+ jobId: videoId,
75
+ },
76
+ };
77
+ }
78
+ async submitVideo(options) {
79
+ const heyOpts = options;
80
+ const avatarId = heyOpts.avatarId ??
81
+ (typeof options.image === "string" &&
82
+ /^[a-zA-Z0-9_-]{20,}$/.test(options.image)
83
+ ? options.image // use image string as avatar_id when it looks like one
84
+ : undefined);
85
+ if (!avatarId) {
86
+ throw new AvatarError({
87
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
88
+ message: "HeyGen requires `avatarId` (HeyGen avatar catalog id). Pass via options.avatarId or as options.image with a valid HeyGen id.",
89
+ category: ErrorCategory.VALIDATION,
90
+ severity: ErrorSeverity.MEDIUM,
91
+ retriable: false,
92
+ });
93
+ }
94
+ if (options.audio !== undefined) {
95
+ if (Buffer.isBuffer(options.audio)) {
96
+ throw new AvatarError({
97
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
98
+ message: "HeyGen requires a publicly accessible audio URL; got a binary Buffer. Upload the audio to a hosted location and pass the HTTPS URL instead.",
99
+ category: ErrorCategory.VALIDATION,
100
+ severity: ErrorSeverity.MEDIUM,
101
+ retriable: false,
102
+ });
103
+ }
104
+ if (typeof options.audio !== "string" ||
105
+ !/^https?:\/\//i.test(options.audio)) {
106
+ throw new AvatarError({
107
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
108
+ message: "HeyGen requires a publicly accessible HTTPS audio URL; got an unsupported audio input type. Upload the audio to a hosted location and pass the HTTPS URL instead.",
109
+ category: ErrorCategory.VALIDATION,
110
+ severity: ErrorSeverity.MEDIUM,
111
+ retriable: false,
112
+ });
113
+ }
114
+ }
115
+ const voiceConfig = options.audio
116
+ ? {
117
+ type: "audio",
118
+ audio_url: options.audio,
119
+ }
120
+ : {
121
+ type: "text",
122
+ input_text: options.text,
123
+ voice_id: options.voice ?? "1bd001e7e50f421d891986aad5158bc8",
124
+ };
125
+ const body = {
126
+ video_inputs: [
127
+ {
128
+ character: {
129
+ type: "avatar",
130
+ avatar_id: avatarId,
131
+ avatar_style: "normal",
132
+ },
133
+ voice: voiceConfig,
134
+ background: {
135
+ type: "color",
136
+ value: heyOpts.backgroundColor ?? "#FFFFFF",
137
+ },
138
+ },
139
+ ],
140
+ dimension: {
141
+ width: heyOpts.width ?? 1280,
142
+ height: heyOpts.height ?? 720,
143
+ },
144
+ };
145
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/video/generate`, {
146
+ method: "POST",
147
+ headers: {
148
+ "X-API-Key": this.apiKey,
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify(body),
152
+ });
153
+ if (!response.ok) {
154
+ const raw = await response.text();
155
+ const retriable = response.status === 408 ||
156
+ response.status === 429 ||
157
+ response.status >= 500;
158
+ throw new AvatarError({
159
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
160
+ message: `HeyGen submit failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
161
+ category: retriable ? ErrorCategory.NETWORK : ErrorCategory.EXECUTION,
162
+ severity: ErrorSeverity.HIGH,
163
+ retriable,
164
+ context: { status: response.status },
165
+ });
166
+ }
167
+ const json = (await response.json());
168
+ const videoId = json.data?.video_id;
169
+ if (!videoId) {
170
+ throw new AvatarError({
171
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
172
+ message: "HeyGen submit response missing video_id",
173
+ category: ErrorCategory.EXECUTION,
174
+ severity: ErrorSeverity.HIGH,
175
+ retriable: false,
176
+ context: { response: json },
177
+ });
178
+ }
179
+ return videoId;
180
+ }
181
+ async pollUntilComplete(videoId, abortSignal) {
182
+ const startTime = Date.now();
183
+ // HeyGen status endpoint is on v1, not v2.
184
+ const statusBaseUrl = this.baseUrl.replace(/\/v2$/, "/v1");
185
+ while (Date.now() - startTime < TOTAL_TIMEOUT_MS) {
186
+ if (abortSignal?.aborted) {
187
+ throw new AvatarError({
188
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
189
+ message: `HeyGen poll for video ${videoId} aborted by caller`,
190
+ category: ErrorCategory.NETWORK,
191
+ severity: ErrorSeverity.MEDIUM,
192
+ retriable: false,
193
+ context: { videoId },
194
+ });
195
+ }
196
+ const response = await this.fetchWithTimeout(`${statusBaseUrl}/video_status.get?video_id=${videoId}`, {
197
+ method: "GET",
198
+ headers: { "X-API-Key": this.apiKey },
199
+ }, abortSignal);
200
+ if (!response.ok) {
201
+ const raw = await response.text();
202
+ throw new AvatarError({
203
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
204
+ message: `HeyGen poll failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
205
+ category: ErrorCategory.NETWORK,
206
+ severity: ErrorSeverity.MEDIUM,
207
+ retriable: response.status >= 500,
208
+ context: { status: response.status, videoId },
209
+ });
210
+ }
211
+ const data = (await response.json());
212
+ if (data.data?.status === "completed") {
213
+ const videoUrl = data.data.video_url;
214
+ if (!videoUrl) {
215
+ throw new AvatarError({
216
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
217
+ message: `HeyGen video ${videoId} completed but no URL returned`,
218
+ category: ErrorCategory.EXECUTION,
219
+ severity: ErrorSeverity.HIGH,
220
+ retriable: false,
221
+ context: { videoId, data },
222
+ });
223
+ }
224
+ return videoUrl;
225
+ }
226
+ if (data.data?.status === "failed") {
227
+ throw new AvatarError({
228
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
229
+ message: `HeyGen video ${videoId} failed: ${data.data?.error?.message ?? "unknown"}`,
230
+ category: ErrorCategory.EXECUTION,
231
+ severity: ErrorSeverity.HIGH,
232
+ retriable: false,
233
+ context: { videoId, data },
234
+ });
235
+ }
236
+ // Abortable sleep.
237
+ await new Promise((resolve, reject) => {
238
+ const onAbort = () => {
239
+ clearTimeout(timer);
240
+ reject(new AvatarError({
241
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
242
+ message: `HeyGen poll for video ${videoId} aborted by caller`,
243
+ category: ErrorCategory.NETWORK,
244
+ severity: ErrorSeverity.MEDIUM,
245
+ retriable: false,
246
+ context: { videoId },
247
+ }));
248
+ };
249
+ const timer = setTimeout(() => {
250
+ abortSignal?.removeEventListener("abort", onAbort);
251
+ resolve();
252
+ }, POLL_INTERVAL_MS);
253
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
254
+ });
255
+ }
256
+ throw new AvatarError({
257
+ code: AVATAR_ERROR_CODES.POLL_TIMEOUT,
258
+ message: `HeyGen video ${videoId} did not complete within ${TOTAL_TIMEOUT_MS / 1000}s`,
259
+ category: ErrorCategory.TIMEOUT,
260
+ severity: ErrorSeverity.MEDIUM,
261
+ retriable: true,
262
+ context: { videoId },
263
+ });
264
+ }
265
+ async download(url) {
266
+ try {
267
+ return await safeDownload(url, {
268
+ maxBytes: MAX_VIDEO_BYTES,
269
+ label: "HeyGen video",
270
+ timeoutMs: REQUEST_TIMEOUT_MS,
271
+ });
272
+ }
273
+ catch (err) {
274
+ throw new AvatarError({
275
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
276
+ message: `HeyGen video download rejected: ${err instanceof Error ? err.message : String(err)}`,
277
+ category: ErrorCategory.NETWORK,
278
+ severity: ErrorSeverity.HIGH,
279
+ retriable: false,
280
+ context: { url },
281
+ originalError: err instanceof Error ? err : undefined,
282
+ });
283
+ }
284
+ }
285
+ async fetchWithTimeout(url, init, callerAbortSignal) {
286
+ const controller = new AbortController();
287
+ let timedOut = false;
288
+ const timeoutId = setTimeout(() => {
289
+ timedOut = true;
290
+ controller.abort();
291
+ }, REQUEST_TIMEOUT_MS);
292
+ const onCallerAbort = () => controller.abort();
293
+ callerAbortSignal?.addEventListener("abort", onCallerAbort, { once: true });
294
+ try {
295
+ return await fetch(url, { ...init, signal: controller.signal });
296
+ }
297
+ catch (err) {
298
+ if (err instanceof Error && err.name === "AbortError") {
299
+ // Check caller abort first — a cancelled request is not a timeout.
300
+ if (callerAbortSignal?.aborted) {
301
+ throw new AvatarError({
302
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
303
+ message: `HeyGen request to ${url} was aborted by the caller`,
304
+ category: ErrorCategory.NETWORK,
305
+ severity: ErrorSeverity.MEDIUM,
306
+ retriable: false,
307
+ originalError: err,
308
+ });
309
+ }
310
+ if (timedOut) {
311
+ throw new AvatarError({
312
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
313
+ message: `HeyGen request to ${url} timed out after ${REQUEST_TIMEOUT_MS / 1000}s`,
314
+ category: ErrorCategory.NETWORK,
315
+ severity: ErrorSeverity.HIGH,
316
+ retriable: true,
317
+ originalError: err,
318
+ });
319
+ }
320
+ // Generic abort (shouldn't happen, but surface it).
321
+ throw new AvatarError({
322
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
323
+ message: `HeyGen request to ${url} was aborted`,
324
+ category: ErrorCategory.NETWORK,
325
+ severity: ErrorSeverity.HIGH,
326
+ retriable: true,
327
+ originalError: err,
328
+ });
329
+ }
330
+ throw err;
331
+ }
332
+ finally {
333
+ callerAbortSignal?.removeEventListener("abort", onCallerAbort);
334
+ clearTimeout(timeoutId);
335
+ }
336
+ }
337
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Replicate Avatar Handler (MuseTalk default)
3
+ *
4
+ * Routes avatar / lip-sync generation through Replicate's universal
5
+ * prediction lifecycle. Default model is MuseTalk; other lip-sync models
6
+ * (SadTalker, Wav2Lip, etc.) can be selected via `options.model`.
7
+ *
8
+ * @module avatar/providers/ReplicateAvatar
9
+ * @see https://replicate.com/douwantech/musetalk
10
+ */
11
+ import type { AvatarHandler, AvatarOptions, AvatarResult, AvatarVideoFormat } from "../../types/index.js";
12
+ /**
13
+ * Replicate Avatar Handler.
14
+ *
15
+ * MuseTalk requires both `image` and `audio` inputs — `text`-only is not
16
+ * supported here (use D-ID for that, or chain TTS + this handler).
17
+ */
18
+ export declare class ReplicateAvatar implements AvatarHandler {
19
+ readonly maxAudioDurationSeconds = 60;
20
+ readonly supportedFormats: readonly AvatarVideoFormat[];
21
+ isConfigured(): boolean;
22
+ generate(options: AvatarOptions): Promise<AvatarResult>;
23
+ private resolveBuffer;
24
+ /**
25
+ * Detect audio MIME subtype from magic bytes.
26
+ *
27
+ * - WAV : "RIFF" header (52 49 46 46)
28
+ * - MP3 : ID3 tag (49 44 33) or sync word 0xFF 0xFB/0xF3/0xF2
29
+ * - OGG : "OggS" capture pattern (4F 67 67 53)
30
+ * - M4A : "ftyp" box at offset 4 (common isom/M4A variant)
31
+ *
32
+ * Falls back to "mp3" when detection is inconclusive.
33
+ */
34
+ private detectAudioType;
35
+ private detectImageType;
36
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Replicate Avatar Handler (MuseTalk default)
3
+ *
4
+ * Routes avatar / lip-sync generation through Replicate's universal
5
+ * prediction lifecycle. Default model is MuseTalk; other lip-sync models
6
+ * (SadTalker, Wav2Lip, etc.) can be selected via `options.model`.
7
+ *
8
+ * @module avatar/providers/ReplicateAvatar
9
+ * @see https://replicate.com/douwantech/musetalk
10
+ */
11
+ import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
12
+ import { AVATAR_ERROR_CODES, AvatarError, } from "../../utils/avatarProcessor.js";
13
+ import { logger } from "../../utils/logger.js";
14
+ import { getReplicateAuth } from "../../adapters/replicate/auth.js";
15
+ import { downloadPredictionOutput, predict, } from "../../adapters/replicate/predictionLifecycle.js";
16
+ import { MAX_AUDIO_BYTES, MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, readBoundedBuffer, } from "../../utils/sizeGuard.js";
17
+ import { assertSafeUrl } from "../../utils/ssrfGuard.js";
18
+ const DEFAULT_MODEL = "douwantech/musetalk:5501004e78525e4bbd9fa20d1e75ad51fddce5a274bec07b9b16d685e34eeaf8";
19
+ /**
20
+ * Replicate Avatar Handler.
21
+ *
22
+ * MuseTalk requires both `image` and `audio` inputs — `text`-only is not
23
+ * supported here (use D-ID for that, or chain TTS + this handler).
24
+ */
25
+ export class ReplicateAvatar {
26
+ maxAudioDurationSeconds = 60;
27
+ supportedFormats = ["mp4"];
28
+ isConfigured() {
29
+ return getReplicateAuth() !== null;
30
+ }
31
+ async generate(options) {
32
+ const auth = getReplicateAuth();
33
+ if (!auth) {
34
+ throw new AvatarError({
35
+ code: AVATAR_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
36
+ message: "REPLICATE_API_TOKEN not configured",
37
+ category: ErrorCategory.CONFIGURATION,
38
+ severity: ErrorSeverity.HIGH,
39
+ retriable: false,
40
+ });
41
+ }
42
+ if (!options.audio) {
43
+ throw new AvatarError({
44
+ code: AVATAR_ERROR_CODES.AUDIO_REQUIRED,
45
+ message: "Replicate avatar handler (MuseTalk) requires `audio` (Buffer or path); text-only is not supported. Use D-ID for text-driven talks or chain TTS + Replicate.",
46
+ category: ErrorCategory.VALIDATION,
47
+ severity: ErrorSeverity.MEDIUM,
48
+ retriable: false,
49
+ });
50
+ }
51
+ const startTime = Date.now();
52
+ const model = options.model ?? DEFAULT_MODEL;
53
+ const imageBuffer = await this.resolveBuffer(options.image, MAX_IMAGE_BYTES, "Replicate avatar reference image");
54
+ const audioBuffer = await this.resolveBuffer(options.audio, MAX_AUDIO_BYTES, "Replicate avatar reference audio");
55
+ const imageDataUri = `data:image/${this.detectImageType(imageBuffer)};base64,${imageBuffer.toString("base64")}`;
56
+ const audioDataUri = `data:audio/${this.detectAudioType(audioBuffer)};base64,${audioBuffer.toString("base64")}`;
57
+ let prediction;
58
+ try {
59
+ prediction = await predict(auth, {
60
+ model,
61
+ input: {
62
+ image: imageDataUri,
63
+ audio: audioDataUri,
64
+ bbox_shift: 0,
65
+ fps: 25,
66
+ },
67
+ });
68
+ }
69
+ catch (err) {
70
+ throw new AvatarError({
71
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
72
+ message: `Replicate avatar generation failed: ${err instanceof Error ? err.message : String(err)}`,
73
+ category: ErrorCategory.EXECUTION,
74
+ severity: ErrorSeverity.HIGH,
75
+ retriable: true,
76
+ context: { model },
77
+ originalError: err instanceof Error ? err : undefined,
78
+ });
79
+ }
80
+ let videoBuffer;
81
+ try {
82
+ videoBuffer = await downloadPredictionOutput(prediction, MAX_VIDEO_BYTES);
83
+ }
84
+ catch (err) {
85
+ throw new AvatarError({
86
+ code: AVATAR_ERROR_CODES.GENERATION_FAILED,
87
+ message: `Replicate avatar download failed: ${err instanceof Error ? err.message : String(err)}`,
88
+ category: ErrorCategory.NETWORK,
89
+ severity: ErrorSeverity.MEDIUM,
90
+ retriable: true,
91
+ context: { predictionId: prediction.id },
92
+ originalError: err instanceof Error ? err : undefined,
93
+ });
94
+ }
95
+ const latency = Date.now() - startTime;
96
+ logger.info(`[ReplicateAvatar] Generated ${videoBuffer.length} bytes in ${latency}ms — model ${model}`);
97
+ return {
98
+ buffer: videoBuffer,
99
+ format: "mp4",
100
+ size: videoBuffer.length,
101
+ provider: "replicate",
102
+ metadata: {
103
+ latency,
104
+ provider: "replicate",
105
+ model,
106
+ jobId: prediction.id,
107
+ },
108
+ };
109
+ }
110
+ async resolveBuffer(input, maxBytes = MAX_IMAGE_BYTES, label = "Replicate avatar input") {
111
+ if (Buffer.isBuffer(input)) {
112
+ if (input.length > maxBytes) {
113
+ throw new AvatarError({
114
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
115
+ message: `${label} too large: ${input.length} bytes exceeds ${maxBytes}`,
116
+ category: ErrorCategory.VALIDATION,
117
+ severity: ErrorSeverity.HIGH,
118
+ retriable: false,
119
+ });
120
+ }
121
+ return input;
122
+ }
123
+ // Reject local file paths — only Buffer or HTTPS URLs are accepted.
124
+ if (!/^https:\/\//.test(input)) {
125
+ throw new AvatarError({
126
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
127
+ message: `Invalid input: expected Buffer or HTTPS URL, got string "${input}". Local file reads are not supported.`,
128
+ category: ErrorCategory.VALIDATION,
129
+ severity: ErrorSeverity.HIGH,
130
+ retriable: false,
131
+ });
132
+ }
133
+ try {
134
+ await assertSafeUrl(input);
135
+ }
136
+ catch (err) {
137
+ throw new AvatarError({
138
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
139
+ message: `Unsafe URL rejected: ${err instanceof Error ? err.message : String(err)}`,
140
+ category: ErrorCategory.VALIDATION,
141
+ severity: ErrorSeverity.HIGH,
142
+ retriable: false,
143
+ context: { url: input },
144
+ });
145
+ }
146
+ const FETCH_TIMEOUT_MS = 60_000;
147
+ const controller = new AbortController();
148
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
149
+ let r;
150
+ try {
151
+ r = await fetch(input, { signal: controller.signal });
152
+ }
153
+ catch (err) {
154
+ if (err instanceof Error && err.name === "AbortError") {
155
+ throw new AvatarError({
156
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
157
+ message: `Replicate avatar input fetch timed out after ${FETCH_TIMEOUT_MS / 1000}s: ${input}`,
158
+ category: ErrorCategory.NETWORK,
159
+ severity: ErrorSeverity.MEDIUM,
160
+ retriable: true,
161
+ });
162
+ }
163
+ throw err;
164
+ }
165
+ finally {
166
+ clearTimeout(timeoutId);
167
+ }
168
+ if (!r.ok) {
169
+ throw new AvatarError({
170
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
171
+ message: `Failed to fetch ${input}: ${r.status}`,
172
+ category: ErrorCategory.NETWORK,
173
+ severity: ErrorSeverity.MEDIUM,
174
+ retriable: r.status >= 500,
175
+ });
176
+ }
177
+ try {
178
+ return await readBoundedBuffer(r, maxBytes, label);
179
+ }
180
+ catch (err) {
181
+ throw new AvatarError({
182
+ code: AVATAR_ERROR_CODES.INVALID_INPUT,
183
+ message: `${label} too large: ${err instanceof Error ? err.message : String(err)}`,
184
+ category: ErrorCategory.VALIDATION,
185
+ severity: ErrorSeverity.HIGH,
186
+ retriable: false,
187
+ context: { url: input },
188
+ });
189
+ }
190
+ }
191
+ /**
192
+ * Detect audio MIME subtype from magic bytes.
193
+ *
194
+ * - WAV : "RIFF" header (52 49 46 46)
195
+ * - MP3 : ID3 tag (49 44 33) or sync word 0xFF 0xFB/0xF3/0xF2
196
+ * - OGG : "OggS" capture pattern (4F 67 67 53)
197
+ * - M4A : "ftyp" box at offset 4 (common isom/M4A variant)
198
+ *
199
+ * Falls back to "mp3" when detection is inconclusive.
200
+ */
201
+ detectAudioType(buffer) {
202
+ if (buffer.length < 4) {
203
+ return "mp3";
204
+ }
205
+ // WAV: starts with RIFF
206
+ if (buffer[0] === 0x52 &&
207
+ buffer[1] === 0x49 &&
208
+ buffer[2] === 0x46 &&
209
+ buffer[3] === 0x46) {
210
+ return "wav";
211
+ }
212
+ // OGG: starts with OggS
213
+ if (buffer[0] === 0x4f &&
214
+ buffer[1] === 0x67 &&
215
+ buffer[2] === 0x67 &&
216
+ buffer[3] === 0x53) {
217
+ return "ogg";
218
+ }
219
+ // MP3: ID3 header
220
+ if (buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) {
221
+ return "mp3";
222
+ }
223
+ // MP3: MPEG sync word (0xFF 0xE0–0xFF)
224
+ if (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0) {
225
+ return "mpeg";
226
+ }
227
+ // M4A / AAC: "ftyp" box at offset 4
228
+ if (buffer.length >= 8 &&
229
+ buffer[4] === 0x66 &&
230
+ buffer[5] === 0x74 &&
231
+ buffer[6] === 0x79 &&
232
+ buffer[7] === 0x70) {
233
+ return "mp4";
234
+ }
235
+ return "mp3";
236
+ }
237
+ detectImageType(buffer) {
238
+ if (buffer.length < 4) {
239
+ return "jpeg";
240
+ }
241
+ if (buffer[0] === 0x89 && buffer[1] === 0x50) {
242
+ return "png";
243
+ }
244
+ if (buffer[0] === 0xff && buffer[1] === 0xd8) {
245
+ return "jpeg";
246
+ }
247
+ // RIFF container: disambiguate WebP (WEBP at offset 8) from WAV (WAVE at
248
+ // offset 8) so audio data passed as image is not silently misidentified.
249
+ if (buffer.length >= 12 &&
250
+ buffer[0] === 0x52 &&
251
+ buffer[1] === 0x49 &&
252
+ buffer[2] === 0x46 &&
253
+ buffer[3] === 0x46) {
254
+ // "WEBP" → image/webp
255
+ if (buffer[8] === 0x57 &&
256
+ buffer[9] === 0x45 &&
257
+ buffer[10] === 0x42 &&
258
+ buffer[11] === 0x50) {
259
+ return "webp";
260
+ }
261
+ // Any other RIFF type (e.g. WAVE) is not a valid image → fall through to
262
+ // the default so the caller's validation can flag the wrong content type.
263
+ return "jpeg";
264
+ }
265
+ return "jpeg";
266
+ }
267
+ }