Compare commits

...
Author SHA1 Message Date
Tanish Manku 8ed02e0961 Merge branch 'refactor/3d-componentize' into refactor/3d-timeline-cleanup 2026-05-08 09:38:51 -07:00
Tanish Manku 4c66c39109 3D editor: drop render-trace debug, clarify upload modal comments
Remove the TEMP render counters from PageEditor / Controls3D / UploadModal3D
now that the inline-component-anti-pattern bug is understood and fixed.
Investigation showed the apparent "4 renders per click" was a StrictMode
dev artifact (one real render, doubled, hitting two UploadModal3D
instances). The single re-render is benign on its own — the bug was
always that <UploaderModalContent /> registered a fresh component
reference each render and remounted the dropzone mid-click.

Update the call-site comment in all five upload modals to spell out
the consequence (file picker breaks) so the inline call form isn't
mistakenly "cleaned up" back to <Comp />.
2026-05-08 09:30:35 -07:00
Brandon ThomasandTanish Manku 58b64b694c Big tauri refactor: new consolidated commands, deprecate and remove duplication (#1486)
Co-authored-by: Tanish Manku <tanish2k09.dev@gmail.com>
2026-05-08 09:29:25 -04:00
BFlat f666e68dcd app: ask location before download (#1483)
- toggle in settings to enable disable ask location on download button
pressed
2026-05-08 16:25:27 +07:00
Brandon Thomas 5a7c253189 fal request logging (#1482) 2026-05-07 21:56:52 -04:00
Brandon Thomas 808beafd4c fal - no api key or webhook url in args (#1481) 2026-05-07 19:28:12 -04:00
bflatastic a10cda3bee website: promptbox body font fix 2026-05-07 21:45:48 +07:00
bflatastic 40f884c776 site font update to align with desktop app 2026-05-07 21:30:33 +07:00
BFlat 9b6b0175ea desktop app: unblock object generations, styling cleanup, model selector (#1478) 2026-05-06 22:51:39 +07:00
BFlat a484b1d1a9 website netlify redirects for support page 2026-05-06 13:01:05 +07:00
BFlat 80d6866fd1 website: support page update (#1477)
- discord first, description change
- navbar support link
- mobile footer icons fix overflow
- scroll to top on page change fix
2026-05-06 12:58:53 +07:00
BFlat ac9a0c2172 show more media info on desktop app lightbox (#1474)
- Lightbox now shows full media metadata: Aspect Ratio, Resolution,
Size, Duration, Audio, and Created (with timestamp).
- Added Author header at the top so the close button no longer overlaps
the Prompt copy button.
- Extracted aspect ratio / resolution / duration formatters into
`@storyteller/common` so the website and desktop app share them.
2026-05-05 21:23:07 +07:00
bflatastic 03c3507841 website: consolidate /session calls into a shared Zustand store 2026-05-05 19:24:23 +07:00
bflatastic 7a7fa63fcc website: mobile menu fixes 2026-05-04 16:42:13 +07:00
bflatastic 30cc15355a support page and mobile nav fix 2026-05-04 16:21:29 +07:00
Brandon Thomas ee16d355f9 moderator debug (#1473) 2026-05-04 02:58:39 -04:00
Brandon Thomas a90a27e45d debug logs (#1472) 2026-05-04 02:18:33 -04:00
Brandon Thomas 769faf3daa fix omni image refs 2026-05-03 16:44:12 -04:00
Brandon Thomas 9d38b45bc2 prompting tool for artcraft (#1470) 2026-05-03 00:26:38 -04:00
Brandon Thomas 4b5e171261 generate cli tool (#1469) 2026-05-02 20:32:29 -04:00
Tanish Manku 82dba6e787 TEMP debug: render-trace UploadModal3D / Controls3D / PageEditor 2026-05-01 23:29:16 -07:00
Tanish Manku 7cc3f2576f Upload modals: comment why content is invoked inline 2026-05-01 22:37:54 -07:00
Tanish Manku 2a4f849940 Upload modals: stop remounting children on every parent re-render
`UploaderModalContent` was defined inline in each upload modal's
function body and rendered as `<UploaderModalContent />`. Each parent
re-render created a fresh component identity, which React treats as
a new component type — unmounting the old subtree and mounting a new
one. PageScene's many zustand subscriptions fire often enough that
this remount sometimes happens between mousedown and mouseup of a
click on the file uploader's drag-drop zone.

Result: original `<u>` ("Upload a file" link) is destroyed mid-click,
mouseup lands on the modal wrapper instead of `<u>`, WebView2 refuses
to synthesize `click`, and the library's native click listener that
opens the OS file picker never fires. Other pages have lighter
parent re-render frequency, so the remount usually doesn't land
inside a click window.

Render `UploaderModalContent` as a function call (`{UploaderModalContent()}`)
instead of a component instance. The function reference still
changes per render but React just sees the returned JSX inline —
no component identity, no remount.

Same fix in all five upload modals so the bug doesn't recur on any
of them.

Also strips the temporary [click-trace] debug logger from
PageEditor.tsx (which identified the cause via mid-click DOM
mutations).
2026-05-01 21:55:23 -07:00
Tanish Manku e8143057b6 TEMP debug: log DOM mutations during click to find what shifts the target 2026-05-01 18:37:46 -07:00
Tanish Manku 989d77f520 Revert "Modal: drop transform from open animation so clicks don't shift targets"
This reverts commit 4167bf22ce.
2026-05-01 18:34:53 -07:00
Tanish Manku 4167bf22ce Modal: drop transform from open animation so clicks don't shift targets
Root cause of the upload-modal file-picker regression: the modal's
React Spring enter spring transitions
`transform: scale(0.95) translateY(-10px)` → `scale(1) translateY(0)`.
With PageScene's heavier render loop, the spring takes long enough
that a click on a small deep child (the file uploader's "Upload a
file" `<u>` link, ~10px tall) lands its mousedown on the `<u>` and
its mouseup on a different element after the modal shifted. WebView2
strictly requires same-target mousedown/mouseup to synthesize
`click`, so no `click` fires, the library's native
`addEventListener("click", u)` on the label never runs, and
`inputRef.current.click()` is never called.

Animate opacity only on enter/leave. Same fade visual, no element
movement under the cursor.

Also strip the now-unused TEMP click tracer from PageEditor.tsx.
2026-05-01 18:27:27 -07:00
Tanish Manku de0d15cd97 TEMP debug: trace mouseup/pointerup + setPointerCapture too 2026-05-01 18:05:36 -07:00
Tanish Manku f91483fe29 TEMP debug: trace click events in PageScene
Logs click / mousedown / pointerdown at document capture + bubble
phases for every event reaching PageScene while it's mounted. Each
log entry shows target, currentTarget, defaultPrevented, and the
first 6 elements of composedPath.

Goal: figure out which listener is silencing the upload-modal
file-picker click. Will be reverted in a follow-up commit.
2026-05-01 17:33:51 -07:00
Tanish Manku ac0c7b2210 PageScene: stop overlay click handler from silencing portal'd modal clicks
Two PageScene-only divergences from how the working pages
(PageDraw / PageImage / PageVideo) handle the same upload modals:

1. `handleOverlayClick` on the top/bottom controls overlays called
   `event.stopPropagation()` for every click that bubbled through
   them. Modals rendered as JSX children of `Controls3D` (the
   upload-3D / upload-image / upload-splat dialogs) are
   React-tree descendants even though `Modal`'s `Dialog.Portal`
   puts them at `document.body` in the DOM. React synthetic
   stopPropagation calls native `Event.stopPropagation()`, which
   silences `react-drag-drop-files`'s native `addEventListener("click", u)`
   on the file-input label — so clicking the drop zone never opens
   the OS file picker.
   Narrow the handler to `event.target === event.currentTarget` so
   it only fires on the transparent overlay area itself, never on
   bubbles from children. The original "control buttons don't
   interact with canvas" intent is preserved without the side
   effect — the canvas listener is bound to the canvas element only,
   so clicks on overlay siblings never reach it regardless.

2. `tabIndex={0}` + `outline: none` on the editor canvas. Added in
   `61c4936c29` "so [the canvas] can receive keyboard focus," but
   `useViewportKeyboard` listens at `document` (not on canvas) so
   the focus isn't actually required. Other pages don't make their
   canvas focusable; remove for parity.
2026-05-01 17:18:07 -07:00
Tanish Manku 6c23ce16dd PromptBox3D: reset commonAspectRatio when new model can't render it
PopoverMenu's toggle mode renders the trigger label from whichever
item has `selected: true` — and AspectRatioPicker only marks an item
selected when `useAspectRatio === ratio` for some ratio in
`model.aspectRatios`. When the user picks an aspect ratio supported
by model A and then switches to model B that doesn't include it,
no item matches, the trigger has no label, and the aspect-ratio text
silently disappears.

Add an effect that, on every `selectedImageModel` change, resets
`commonAspectRatio` to the new model's default if the current pick
isn't in the new model's `aspectRatios`. Routes through
`handleCommonAspectRatioSelect` so the editor letterbox follows the
reset (the user's pick was unrenderable on this model anyway, so the
fallback is correct here — distinct from the cold-load case).
2026-05-01 16:34:19 -07:00
Tanish Manku 00f4399ccd PageScene: cold-load letterbox sync only — don't reapply on model switch
Almost every entry in ImageModels.ts declares
`defaultAspectRatio: Square`, so the per-model-switch sync was
forcing the letterbox to Square on every model swap and overriding
whatever the user had picked.

Replace `lastSyncedModelIdRef` with a single `didColdSyncRef` so
the sync fires exactly once — when the engine and a
`supportsNewAspectRatio()` model are both ready — and then never
again. Subsequent letterbox changes flow only through user picks via
PromptBox3D's `handleCommonAspectRatioSelect`.
2026-05-01 16:29:00 -07:00
Tanish Manku 9bd98447ce PageScene: sync letterbox to model default once engine is ready
The PromptBox3D-side cold-load sync raced: the effect fires when
`selectedImageModel` becomes defined, but `editorEngine` is still
null at that point because EngineProvider initializes asynchronously
(waits on canvas refs landing in the store). `onAspectRatioSelect`
early-returns when there's no engine, so the letterbox never gets
the model's default applied.

Move the sync into PageEditor with `[editorEngine, selectedImageModel]`
deps, gated by a ref that records the last synced model id so a later
user pick on the same model isn't clobbered.
2026-05-01 16:22:14 -07:00
Tanish Manku 1ee30284df PromptBox3D: drive letterbox from the new aspect-ratio picker
For models with `supportsNewAspectRatio()`, the picker only stored a
`CommonAspectRatio` in local state for the API request payload — it
never called `onAspectRatioSelect`, so the editor's render-camera
aspect and the Letterbox stayed on the previous setting.

Wrap the picker callback to also map the chosen `CommonAspectRatio`
to the closest `CameraAspectRatio` and forward it through
`onAspectRatioSelect`. `Auto*` ratios have no fixed shape, so they
leave the letterbox alone.

Export `commonToCameraAspect` so PageEditor can reuse the same
mapping for the cold-load sync.
2026-05-01 16:22:05 -07:00
Tanish Manku 2725281775 3D: Fix broken freecam regression after refactor (#1468) 2026-05-01 06:28:15 -07:00
Tanish Manku ab4b33a966 3D transform control panel + undo/redo fixes (#1467) 2026-05-01 05:05:22 -07:00
Tanish Manku 3bfb405e8e 3D SaveManager cleanup and History Manager architecture (#1466) 2026-05-01 04:06:08 -07:00
Tanish Manku 5e05877619 3D editor.ts cleanup (#1461) 2026-05-01 00:10:50 -07:00
Tanish Manku fec717057e Re-add shaders.js used by chromakey.js 2026-04-30 14:32:53 -07:00
Tanish Manku 9ad3b86524 Audio: Move sound system into SoundManager, fix bootstrap + master toggle (#1455)
- Move sound catalog registration off PageScene mount and into a single
SoundManager.install() call in GlobalSettingsManager so sounds exist at
app boot (previously silent until PageScene mounted).
- Honor the play_sounds master toggle in all five event playback sites
via SoundManager.playEvent helpers. Settings preview keeps firing
regardless (handled by SoundManager.playPreview).
- Replace per-callsite GetAppPreferences + SoundRegistry boilerplate in
the four generation event hooks and the file-deleted handler with
one-line SoundManager.* calls.
- Dedupe SOUND_OPTIONS: AudioSettingsPane now reads
SoundManager.OPTIONS, derived from a single declarative SOUNDS array
(label-bearing entries are user-selectable; others are still registered
for code-driven use).
- Narrow the soundboard public barrel to SoundManager + SoundOption
only; SoundRegistry and SoundEffect are now lib-internal.
2026-04-30 09:07:28 -07:00
Tanish Manku 2b5e509a29 PageScene: move engine lifecycle flags + canvas refs into PageSceneStore
The last batch of PageScene-owned signals — `is3DPageMounted`,
`is3DEditorInitialized`, `is3DSceneLoaded`, and the three canvas-ref
signals (sceneContainer, editorCanvas, camViewCanvas) — now live on
the Zustand store. EngineProvider, the canvas components, the
useEditorCanvas hook, and the cross-app TopBar consumer all read from
the store instead. The helper signals in editor.ts are gone; the
`set3DPageMounted` / `setIs3DEditorInitialized` / `setIs3DSceneLoaded`
exports stay as thin wrappers around the store actions for existing
callers.
2026-04-29 15:16:29 -07:00
Tanish Manku 28db87c1b2 PageScene + libs: drop camera/promptBox signals, switch shared libs to plain props
PromptBox3D and CameraSettingsModal now accept plain values + setters
instead of `Signal<T>` props. The camera/focalLengthDragging/
isPromptBoxFocused signals are gone — store is the single source of
truth. Engine code (editor/save_manager/scene) reads/writes via
`usePageSceneStore.getState()` and store actions. Demo asset signals
are now plain constants (they were never reactive).
2026-04-29 15:12:13 -07:00
Tanish Manku 292cf33f57 PageScene: route setCameraAspect + save_manager directly through PageSceneStore 2026-04-29 15:00:48 -07:00
Tanish Manku 58b7cd0cd4 PageScene: route useFreeCam through PageSceneStore for prompt-box focus check 2026-04-29 14:59:54 -07:00
Tanish Manku 101b735c35 PageScene: migrate outliner signals to PageSceneStore 2026-04-29 14:59:06 -07:00
Tanish Manku 32291ce080 PageScene: migrate objectPanel signals to PageSceneStore 2026-04-29 14:56:05 -07:00
Tanish Manku cd9c06d2ce PageScene: migrate precisionSelector signals to PageSceneStore 2026-04-29 14:54:24 -07:00
Tanish Manku a780779f7a PageScene: migrate dragAndDrop signals to PageSceneStore 2026-04-29 14:53:10 -07:00
Tanish Manku 076b7fe4a4 PageScene: extend PageSceneStore with drag/objectPanel/outliner/precisionSelector fields 2026-04-29 14:52:05 -07:00
Tanish Manku 7eef728d22 PageScene: trim api_manager + drop dead model/error files
api_manager.ts goes from 460 lines to ~135. The video-render
pipeline (uploadMediaFrameGeneration, stylizeVideo,
StylizeVideoParams, getMediaBatch, getMediaFile, uploadMedia,
gltfExport) was unreachable after videoGeneration was deleted.
Surviving public methods:

- saveSceneState (called by save_manager)
- loadSceneState (called by save_manager)
- uploadMediaSceneThumbnail (used internally by saveSceneState)

Also drops the local ArtStyle / Visibility / MediaFile types —
they only fed the deleted methods. The ArtStyle enum has a
separate copy in ~/enums/ArtStyle.ts that other code uses.

Delete two files that became unreferenced:
- engine/APIManagerResponseError.ts (duplicate of the local class
  in api_manager.ts; nothing imported it)
- models/generationOptions.ts (IGenerationOptions/GenerationOptions
  fed editor.generation_options, deleted last commit)

Net: -1 typecheck error, -326 lines.
2026-04-29 14:12:03 -07:00
Tanish Manku d09c7320fd PageScene: delete misc dead files surfaced by stylize cleanup
Files with zero importers across the artcraft app:

- constants/RandomTexts.ts — RandomTextsPositive/Negative seeded
  prompt suggestions in the deleted StylizeTab.
- composable_engines/scene_format.tsx — empty (1 line).
- constants/page.ts and the matching currentPage signal in
  signals/global.ts — Pages enum + signal, neither read.
- enums/StylizeTabPages.ts and enums/AudioTabPages.ts — re-exported
  via enums/index.ts but no consumer.
- models/track.ts, models/message.ts, models/voice.ts, models/tts.ts
  — timeline / TTS / audio-panel types from deleted features.
- styleList.ts — used to seed selectedArtStyle's default; that
  store field is gone.
- models/types.tsx had a duplicate AudioTabPages enum (separate
  from the deleted one in enums/) — drop the duplicate too.

models/index.ts and enums/index.ts trimmed to match. Error count
unchanged (151) — none of these had broken type errors anyway.
2026-04-29 14:08:08 -07:00
Tanish Manku 0ca91e29b7 PageScene: drop orphaned stylize state
Strip the stylize fields that nothing reads anymore now that the
StyleMoreOptions / StyleSelection UI is gone. From PageSceneStore:
selectedArtStyle, upscale, faceDetail, styleStrength, lipSync,
cinematic, enginePreProcessing, globalIPAMediaToken, adapterImage,
positivePrompt, negativePrompt, isUserInputPositive,
isUserInputNegative, showNegativePrompt — and the matching setters
+ resetSceneGenerationMetadata. Drop the styleList / ArtStyle
imports; both go unused here.

From editor.ts: drop negative_prompt, art_style,
engine_preprocessing, generation_options, globalIpAdapterImage.
Keep positive_prompt — PromptBox3D writes it via setEnginePrompt
(PageEditor.tsx) and save_manager round-trips it.

Trim save_manager.loadFromJson to the prompt-only path (legacy
saves with stylize keys load fine; the now-removed if-guards just
silently ignored them anyway). Trim SceneMetadata to
{ positivePrompt, cameraAspectRatio } and the matching
SceneGenereationMetaData interface.

ControlsTopButtons.handleResetScene drops its now-removed store
call.
2026-04-29 14:07:03 -07:00
Tanish Manku 8af4657393 PageScene: delete orphan StyleMoreOptions and StyleSelection UI
Both component trees survived earlier cleanup but never render
from PageEditor or PageScene. They were the last UI consumers
of the stylize store fields; deleting them sets up a follow-up
that drops those store fields entirely.

Net: -17 typecheck errors (the orphan files were the source of
many missing-import errors).
2026-04-29 14:04:02 -07:00
Tanish Manku f8ee94e538 Revert moodboard-html files committed by mistake on this branch
cardLayout.ts, MoodboardHtmlColorPicker.tsx, findDropTarget.ts, and
CardNodeHtml.tsx were swept into the PageEnigma -> PageScene rename
commit (76add858c9) on this 3D-refactor branch. They belong on
moodboard-page-html.

The file content is preserved on the local-only moodboard-rescue
branch (pointing at 01e0e27037). To carry them back over:

  git checkout moodboard-page-html
  git checkout moodboard-rescue --     frontend/apps/artcraft/app/src/pages/PageMoodboard/cardLayout.ts     frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/MoodboardHtmlColorPicker.tsx     frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/interactions/findDropTarget.ts     frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/nodes/CardNodeHtml.tsx
  git commit
2026-04-29 12:29:58 -07:00
Tanish Manku 01e0e27037 PageScene: route PoseModeSelector through getActiveEditor()
The Enter Pose Mode button reached through a window.__mouseControls
global handle that MouseControls' constructor set up. With
getActiveEditor() already exposing the live editor instance for
non-React callers (used in the camera-view toggle, the keymap),
the global is unnecessary.

Drop the (window as any).__mouseControls = this assignment in
MouseControls and the matching Window augmentation in
PoseModeSelector. Replace the click handler's reach with
getActiveEditor()?.mouse_controls?.toggleFKMode() — same shape
the keymap entry already uses.
2026-04-29 11:33:30 -07:00
Tanish Manku 794f65b517 PageScene: delete unused models/sidePanel.ts
The Tab interface in this file was the type for the deleted
SidePanel/tabList.tsx tab list. Grep across app/src/ confirms no
consumer remains; AssetModal uses its own local AssetTab type.
Drop the file and the matching re-export in models/index.ts.
2026-04-29 11:32:48 -07:00
Tanish Manku c53b407952 PageScene: drop unused imports surfaced by deletions
EditorStates is no longer referenced from editor.ts now that
PREVIEW is gone from the enum and switchPreview/switchEdit are
deleted. EngineCanvases.tsx never used useContext/EngineContext;
the imports were leftover from earlier work.

Net: -3 typecheck errors.
2026-04-29 10:03:23 -07:00
Tanish Manku e0611e0147 PageScene: drop document.mouseover gate + dead selectedCanvas
The mouseOverEventHandler / enableFreeCamControls /
disableFreeCamControls trio existed to track when the cursor was on
the viewport canvas and toggle FreeCam.enabled. With useFreeCam
gating itself on editorState === CAMERA_VIEW, that gate is
redundant; the only remaining consumer of selectedCanvas was
MouseControls.onkeydown's space-bar branch, which is unreachable
after the previous keyboard rewrite.

Removes the last document.addEventListener call from PageScene.
2026-04-29 10:02:09 -07:00
Tanish Manku 8a03449128 PageScene: keymap + useViewportKeyboard hook
Replace MouseControls.onkeydown's giant if-else chain with a
declarative keymap:

- engine/keymap.ts: KeyBinding type + buildKeymap() returning every
  one-shot viewport shortcut as one entry. Each carries label /
  group metadata so a future Ctrl-hold cheatsheet overlay can read
  the same array. dispatchBinding() does the modifier match
  (treating Ctrl and Meta as equivalent for macOS).
- hooks/useViewportKeyboard.ts: attaches a single keydown listener
  that gates on hotkeyStatus + editable-element checks, then walks
  the keymap. Held movement keys (W/A/S/D etc.) live in useFreeCam
  since they're continuous; this hook is for one-shots only.

The listener is on document, not the canvas, deliberately: canvas
focus is finicky and would require the user to click the viewport
before T/R/G work. The editable-element guard prevents the keymap
from firing while inputs are focused.

MouseControls.onkeydown is now unreachable (SceneManager's window
listeners are no-op since the previous commit, and useViewportKeyboard
goes straight to keymap dispatch). The class itself stays for now —
it still implements selection / FK / pointer-lock state.
2026-04-29 10:00:56 -07:00
Tanish Manku e4af73c9ac PageScene: useViewportPointer hook (canvas-scoped)
The old SceneManager.attachEventListeners() attached window-level
mousemove / click / mousedown / mouseup / keydown handlers in dev
mode and dispatched them into MouseControls. Replace with two pieces:

- hooks/useViewportPointer.ts: canvas-scoped pointer listeners
  that call into editor.mouse_controls. NDC pointer coords are
  computed via the new engine/pointer.ts helpers (ndcFromClient,
  applyNdcToVector2). Selection clicks suppress while the
  free-cam owns input (CAMERA_VIEW state).
- engine/pointer.ts: pure NDC coordinate translation, no DOM
  side-effects.

SceneManager.attachEventListeners / detachEventListeners are now
no-ops; kept for back-compat with the editor lifecycle so future
follow-ups can carve them out cleanly.

MouseControls itself still owns selection / FK / drag state — its
dispatch methods (onMouseMove, onMouseClick, etc.) are now invoked
by the React hook instead of window listeners. A future PR can
dissolve MouseControls further; the goal here is "no window
listeners" + "React owns lifecycle".
2026-04-29 09:58:24 -07:00
Tanish Manku efa9a54bee PageScene: useFreeCam hook + delete free_cam.ts
The 446-line FreeCam class is gone. Its responsibilities split:

- Math (move/rotate vectors, pan/zoom from inputs, lookAt) lives in
  cameraMath.ts as pure functions, plus a freeCamFrameTick() that
  integrates the per-frame state.
- Listener attachment + drag/key state ownership moves into
  hooks/useFreeCam.ts. Listeners are scoped to the canvas (no
  window/document handlers); held-key state is reset on
  CAMERA_VIEW exit so a stale W keypress can't drift the camera.
- The shared FreeCamControlState struct flows through Editor via
  editor.setFreeCamState(state); the render loop reads it and
  calls freeCamFrameTick() each frame.

Side effects: keybinds_controls.ts now imports
isPointerLockSupported from browserChecks; helper.ts and
PageEditor's camera-select handler stop poking
.cameraViewControls and reset velocity via freeCamState instead.
PreviewEngineCamera's camera-view toggle now just flips the store
state — useFreeCam picks it up on its own (no more
enableFreeCamControls/disableFreeCamControls call site).
2026-04-29 09:55:40 -07:00
Tanish Manku 718b8261fd PageScene: extract cameraMath + browserChecks modules
Pure-function helpers carved out of FreeCam in preparation for the
hook rewrite:

- engine/cameraMath.ts: HeldMoveKeys / HeldRotateKeys types,
  moveVectorFromKeys, rotationVectorFromKeys, panFromDrag,
  zoomFromWheel, lookAtFromCamera, lerpVelocity. No DOM access, no
  signals.
- engine/browserChecks.ts: isSafariOrWebKit, hasApplePay, isSafari,
  isPointerLockSupported. Unrelated to camera math; were just
  hitchhiking in free_cam.ts.

free_cam.ts re-exports the browser checks for back-compat with
keybinds_controls.ts; both consumers will switch to the direct
imports when free_cam.ts is deleted in the next commit.
2026-04-29 09:50:16 -07:00
Tanish Manku 61c4936c29 PageScene: useEditorCanvas hook + tabIndex on viewport canvas
The Phase 6 hooks (useFreeCam, useViewportPointer, useViewportKeyboard)
need a React-shaped accessor for the canvas DOM node so their
useEffects re-run when the canvas mounts/unmounts. Promote the
existing editorCanvasSignal / camViewCanvasSignal into useEditorCanvas
/ useCamViewCanvas hooks that bridge signal -> React state via a
subscribe.

Add tabIndex={0} and outline:none to the editor canvas so it can
receive keyboard focus without showing a focus ring.
2026-04-29 09:48:59 -07:00
Tanish Manku c23e652499 PageScene: wire camera-view toggle button
handleButtonCameraView in PreviewEngineCamera was a TODO no-op.
Flip editorState (EDIT <-> CAMERA_VIEW) via the store, then call
editor.enableFreeCamControls() / disableFreeCamControls() on the
active editor. Drop the stale console.log useEffect that fired on
every state change.
2026-04-29 09:48:02 -07:00
Tanish Manku a26ebce483 PageScene: delete video generation entirely
The 3D editor no longer generates videos. Remove:

- engine/video_generation.ts (the VideoGeneration class)
- comps/GenerateModals/ (modal + MyMovies/Sharing/Activity/cards)
- comps/PreviewFrameImage.tsx (PREVIEW-state-only preview overlay)
- comps/ControlsTopButtons/TestFeaturesButtons.tsx (dev-only,
  rendered with debug={false})
- ~/components/signaled/TopBar/MyMoviesButton.tsx (was orphaned;
  no consumer)

editor.ts: drop the VideoGeneration import, field, instantiation,
and the dead helpers it powered (useCachedMediaTokens,
stopPlaybackAndUploadVideo, switchPreview/switchEdit, generateFrame,
generateVideo, checkAndUseCache).

PageSceneStore: drop previewSrc/viewMyMovies/generateMovieId/
generateProgress fields and setters.

EditorStates: drop the PREVIEW variant; PageEditor's state machine is
now EDIT <-> CAMERA_VIEW only. PREVIEW was set only by the deleted
video_generation.ts and read by PreviewFrameImage (deleted) and a few
gates in PreviewEngineCamera + ControlPanelSceneObject (now removed).

Net: -8 typecheck errors.
2026-04-29 09:47:14 -07:00
Tanish Manku 1d3da065a2 PageScene: drop dead side-panel store fields
sidePanelWidth, sidePanelVisible, dndSidePanelWidth and their setters
are no longer read by any rendered component. Drop them from
PageSceneStore and the lone setSidePanelWidth(340) call in
PageEditor's mount effect. PreviewFrameImage's width calc no longer
subtracts the (always-zero) panel width.
2026-04-29 09:42:38 -07:00
Tanish Manku b4a3eb88bf PageScene: delete dead side-panel tree
The <SidePanel /> component was never rendered from PageEditor.tsx; the
entire comps/SidePanel/, comps/SidePanelMenu/, and most of comps/SidePanelTabs/
tree was unreachable. Relocate the few files still consumed by
AssetModal (sharedComps/ItemElement{,s}.tsx, hooks/useUserObjects,
hooks/useFeaturedObjects, utilities/fetchMediaItems + misc) to
comps/AssetMenu/{shared,hooks,utilities}/, and the StyleOptions panel
used by StyleMoreOptions to its parent folder. Then delete the rest.

Net: -33 typecheck errors (the dead tree had its own type problems).
2026-04-29 09:41:21 -07:00
Tanish Manku 48ab5f73ee Phase 7: rename Editor/ -> engine/ and drop dead useSignals() calls
Editor/ -> engine/:
- Rename pages/PageScene/Editor/ -> pages/PageScene/engine/
- Update every "./Editor/...", "../Editor/...", "../../Editor/...",
  "~/pages/PageScene/Editor/..." import path. The class name `Editor`,
  `EditorState` types, and `editorState` field are unchanged -- only
  the directory is renamed
- ImageTo3DExperience also gets the new path

useSignals() drops where the component reads zero .value signals:
- comps/GenerateModals/Activity.tsx
- comps/SidePanelTabs/hooks/useFeatureFlags.tsx
- comps/SidePanelTabs/sharedComps/ItemElement.tsx
- comps/SidePanelTabs/tabComps/StylizeTab/PageStyleSelection.tsx
- comps/StyleMoreOptions/StyleMoreOptions.tsx

The remaining useSignals() callers still subscribe to genuine signals
(pageHeight/pageWidth from ~/signals, outlinerIsShowing,
sceneContainerSignal, etc.) and need to stay until those signals migrate.

Final grep audit confirms no residue:
- "PageEnigma" -- 0 hits
- "from \"...Editor/...\"" import paths -- 0 hits
- Queue.publish / Queue.subscribe / fromEngineActions /
  toEngineActions / toTimelineActions -- 0 hits
- TimeLine, AudioEngine, TransformEngine, EmotionEngine, LipSyncEngine,
  CharacterAnimationEngine, ClipUI, TimelineState -- 0 hits

Typecheck unchanged at 144 errors in PageScene; zero new errors.
2026-04-28 23:47:34 -07:00
Tanish Manku 76add858c9 PageEnigma -> PageScene: rename to match the route param and the page title
The directory was named after a stale codename. The route is
/$sceneToken, the top bar reads "Edit Scene", and the page is the main
scene-editing workspace. Renaming the directory, top-level component,
and Zustand store accordingly:

- pages/PageEnigma/ -> pages/PageScene/
- pages/PageScene/PageEnigma.tsx -> PageScene.tsx
- pages/PageScene/PageEnigmaStore.ts -> PageSceneStore.ts
- usePageEnigmaStore -> usePageSceneStore (function rename across the
  whole codebase via sed)
- All ~/pages/PageEnigma/* import paths -> ~/pages/PageScene/*
- Component name PageEnigma -> PageScene in routes/_index.tsx and
  routes/$sceneToken.tsx

234 files touched, all mechanical. Typecheck error count holds at 144;
zero new errors introduced by the rename.
2026-04-28 23:44:26 -07:00
Tanish Manku e39e67195e PageEnigma refactor Phase 5g: migrate stylizeTab signal slice to the store
stylizeTab.ts (deleted). 11 consumers migrated:

UI components:
- StyleOptions: cinematic / upscale / faceDetail / lipSync /
  enginePreProcessing toggles drive the matching store setters; the
  upscale<->cinematic mutual-exclusion logic preserved
- StyleStrength: slider + number input read styleStrength from store,
  call setStyleStrength
- Prompts: textBufferPositive / textBufferNegative now positivePrompt /
  negativePrompt on the store; isUserInputPositive /
  isUserInputNegative / showNegativePrompt as fields with setters
- IPAdapter: adapterImage + globalIPAMediaToken from store; useShallow
  subscribes to all four needed fields/setters in one selector
- StylizeTab: selectedArtStyle from store, setSelectedArtStyle replaces
  the old setArtStyleSelection helper
- StyleSelection: same pattern -- selectedArtStyle and
  setSelectedArtStyle
- GenerateMovieButton: snapshots the entire stylize bag from the store
  with usePageEnigmaStore.getState() inside the click handler

Engine + persistence:
- SceneMetadata.getSceneGenerationMetaData reads cameraAspectRatio,
  globalIPAMediaToken, upscale, faceDetail, styleStrength, lipSync,
  cinematic, enginePreProcessing all from the store in one getState()
- save_manager.loadFromJson restores all stylize state through the
  store's setters instead of the old restoreSceneGenerationMetadata
  signal helper. globalIpAdapterImageMediaToken restoration also goes
  through setGlobalIPAMediaToken
- video_generation: setGlobalIPAMediaToken on upload + reads
  globalIPAMediaToken from store for the gen request payload
- ControlsTopButtons: new-scene flow calls
  usePageEnigmaStore.getState().resetSceneGenerationMetadata()

Bonus cleanup:
- SidePanelTabs reverted to inlining pageHeight - 64 (the previous
  edit got lost in a stash round-trip)
- IPAdapter dropped useSignals and uses useShallow for its 4-field
  selector

Net effect on typecheck: 145 -> 144 errors in PageEnigma.
2026-04-28 23:28:55 -07:00
Tanish Manku ea49ddf60a PageEnigma refactor Phase 5f: migrate sizing slice to the store
sizing.ts (deleted). Was already minimal -- just sidePanelWidth, the
last survivor after the timeline-related sizing exports were stripped
in Phase 2. Three consumers migrated:

- PageEditor's mount-time setup: store.setSidePanelWidth(340)
- PreviewFrameImage: pulls sidePanelWidth off the store alongside
  sidePanelVisible
- SidePanel: same pattern (already reading sidePanelVisible /
  dndSidePanelWidth from the store; sidePanelWidth joins them)
- StyleSelection: subscribes to sidePanelWidth via the store

signals/ down to 10 files: camera, demoAssets, dragAndDrop, engine,
objectPanel, outliner, precisionSelectorMenu, promptBox, stylizeTab.
2026-04-28 23:22:57 -07:00
Tanish Manku e0c04efa8f PageEnigma refactor Phase 5e: migrate selectedMode + sidePanel slices
selectedMode.ts (deleted):
- PoseModeSelector reads poseMode + showPoseControls from store, calls
  setPoseMode to toggle
- keybinds_controls migrates the t/r/g hotkeys (mode change),
  x (transform space toggle), 'Escape' (exits pose mode), object
  selection (sets showPoseControls), and FK toggle to the store
- editor.change_mode + toggleTransformSpace read/write transformSpace
  through usePageEnigmaStore.getState()
- Controls3D's mode dropdown + world/local pill read selectedMode and
  transformSpace from the store
- actions/setTransformMode no longer dual-writes the legacy signal
- store's PoseMode union changed from "select" | "edit" to
  "select" | "pose" to match what the engine actually checks

sidePanel.ts (deleted):
- SidePanel reads sidePanelVisible + dndSidePanelWidth from store
- TabTitle's close button calls setSidePanelVisible
- SidePanelMenu pulls sidePanelVisible + setSidePanelVisible
- PreviewFrameImage reads sidePanelVisible (was a signal, now store)
- sidePanelHeight (was a computed signal of pageHeight - 64) inlined
  at its three call sites: PreviewEngineCamera, SidePanelTabs,
  StyleSelection. cameraFilter has no consumers; it lives on the store
  for the next round.

signals/ down to 11 leaves: camera, demoAssets, dragAndDrop, engine,
objectPanel, outliner, precisionSelectorMenu, promptBox, sizing,
stylizeTab.
2026-04-28 23:21:13 -07:00
Tanish Manku dc140c0bc1 PageEnigma refactor Phase 5d: migrate hotkeys + selectedItem signal slices
hotkeys.ts (deleted):
- keybinds_controls reads usePageEnigmaStore.getState().hotkeyStatus.disabled
- ControlPanelSceneObject pulls disableHotkeyInput / enableHotkeyInput
  off the store and passes them to InputVector
- PageEditor pulls the same and passes them to LoginModal +
  PromptBox3D. DomLevels imported from PageEnigmaStore (replacing the
  signals/hotkeys re-export)

selectedItem.ts (deleted):
- No external consumers (dead since the timeline removal).
  selectedObject lives on the store; the legacy signal had no readers
  outside its own file.

signals/index.ts no longer re-exports either slice. Down to 13 files
in signals/.
2026-04-28 23:15:45 -07:00
Tanish Manku 21d3617970 PageEnigma refactor Phase 5c: migrate assetModal signal slice to the store
assetModal.ts (deleted):
- AssetMenu: setAssetModalVisibleDuringDrag(true) on open
- AssetModal: reads assetModalVisible / assetModalVisibleDuringDrag /
  reopenAfterDrag from the store, writes via the matching setters.
  Modal close drives setAssetModalVisible(false); the reopen-after-add
  checkbox writes through setReopenAfterDrag (note: store field is
  reopenAfterDrag, not the old reopenAfterDragSignal name)
- Controls3D: handleOpenModal flips both setters from the store; the
  empty-scene tooltip useEffect reads assetModalVisible from the store
- keybinds_controls: 'b' shortcut opens the asset modal via the store
- DragAndDrop/DndAsset: pointerdown clears
  assetModalVisibleDuringDrag, pointerup restores it from
  reopenAfterDrag, click (no drag) restores it to true. All through
  the store now.

signals/index.ts no longer re-exports assetModal. Four signal files
gone so far in Phase 5; signals/ now down to 13 leaves (camera,
demoAssets, dragAndDrop, engine, hotkeys, objectPanel, outliner,
precisionSelectorMenu, promptBox, selectedItem, selectedMode,
sidePanel, sizing, stylizeTab).
2026-04-28 23:13:57 -07:00
Tanish Manku 11a0ac82b6 PageEnigma refactor Phase 5b: migrate error, myMovies, editorOverlays signal slices
Three more signal files retired in favour of the Zustand store.

error.ts (deleted):
- ErrorDialog (in ~/components/signaled): reads showErrorDialog /
  errorDialogTitle / errorDialogMessage from the store, calls
  setShowErrorDialog
- SceneTitleInput (in TopBar): handleShowErrorDialog uses the store's
  setErrorDialog action

myMovies.ts (deleted):
- MyMoviesButton (in TopBar): writes via setGenerateMovieId /
  setViewMyMovies
- GenerateModals: useEffect-on-store replaces useSignalEffect; reads
  generateMovieId from store
- MyMovies and Sharing: read viewMyMovies from store, close via
  setViewMyMovies
- Sharing also reads generateMovieId from store

editorOverlays.ts (deleted):
- PageEditor's LoadingDots: reads editorLoader from store
- SceneContainer: reads editorLetterBox from store and passes through
  to Letterbox
- PreviewEngineCamera: reads editorLetterBox + toggleEditorLetterBox
  from store
- save_manager: showEditorLoader/hideEditorLoader become thin wrappers
  over the store actions, keeping the same call sites

signals/index.ts no longer re-exports any of the three deleted files.
2026-04-28 23:11:52 -07:00
Tanish Manku 4b0618e50b PageEnigma refactor Phase 5a: migrate engine.ts signal slice to the store
Migrates components and engine code from reading signals/engine.ts
(cameraAspectRatio, editorState, gridVisibility, previewSrc) to reading
the Zustand store.

Components migrated to usePageEnigmaStore reads:
- AspectRatioMenu (also dropped useSignals + collapsed two
  duplicated lookup ladders into ICON_BY_RATIO / LABEL_BY_RATIO)
- PreviewFrameImage
- Letterbox (no longer reactive via signals; reads camAspect from store)
- StyleButtons (editorState, previewSrc, setEditorState)
- Outliner (cameraAspectRatio)
- SidePanelMenu (editorState + setEditorState)
- ControlPanelSceneObject (editorState)
- PreviewEngineCamera (cameraAspectRatio + editorState)
- PageEditor (cameraAspectRatio for getScale; the signal version still
  flows into PromptBox3D as a prop)

Engine code migrated to usePageEnigmaStore.getState():
- editor.ts: snapShotOfCurrentFrame reads cameraAspectRatio + grid;
  switchPreview / switchEdit set editorState
- helper.ts: switchCameraView writes editorState
- video_generation.ts: generateFrame writes previewSrc + editorState
- SceneMetadata.ts: cameraAspectRatio for snapshot metadata
- scene.ts: subscribes to store gridVisible (not the signal)

signals/engine.ts becomes a thin compatibility layer for PromptBox3D's
signal-typed props. Its setters dual-write into the store so any
PromptBox3D-driven changes (grid toggle, aspect ratio dropdown) keep
the store authoritative. editorState / previewSrc / setEditorState
removed entirely -- nothing outside the store needs them anymore.

Net effect on typecheck: 148 -> 147 errors in PageEnigma. Zero new
errors introduced.
2026-04-28 23:05:54 -07:00
Tanish Manku 35aae54d49 fix: let UploadSuccess actually render in upload modals
The auto-success useEffect in UploadModalSplat / UploadModalImage /
UploadModal3D was firing on UploaderStates.success and immediately
calling onClose(), which short-circuited the UploaderModalContent
switch before the UploaderStates.success branch (UploadSuccess) could
render. The OK button on UploadSuccess was effectively dead code.

Move the success-side actions (open the gallery, fire onSuccess, close
the modal) into the UploadSuccess onOk handler. The user now sees the
"upload successful" dialog and can dismiss it intentionally; the
gallery opens at dismissal so they can drag the freshly uploaded asset
onto the canvas.
2026-04-28 22:56:38 -07:00
Tanish Manku 088edd270f UploadModalSplat: open gallery on success to match image / 3D-model upload
Pre-refactor the splat upload modal had two paths: the (developer-only)
local-bytes path that handed the in-memory buffer to a now-deleted
editor.timeline.addLocalSplat, and a real upload path that registered
the file in Storyteller's media library but did nothing else with it.

The local-bytes branch was the only thing letting a freshly uploaded
splat reach the canvas. With it gone, splat upload was a dead end --
the file landed in the user's media library but they had no path back
to the 3D editor.

Align with the existing image / 3D-model upload convention: on
UploaderStates.success, set galleryModalVisibleViewMode and call
onSuccess(selectedCategory). The user can then drag the freshly
uploaded splat from the auto-opened gallery onto the canvas, which
flows through PageEditor's onImageDrop handler and into
actions/addObject.

Other changes:
- onSuccess signature: (ArrayBuffer, boolean) => (FilterEngineCategories)
  to match UploadModalImage / UploadModal3D
- Drop the unused onLocalBytes prop and plumbing in UploadFilesSplat;
  no callsite invoked it. The two existing consumers
  (PageEnigma/Controls3D, GlobalFileDropHandler) ignore the category
  argument, which is fine -- () => void is assignable.
2026-04-28 22:50:17 -07:00
Tanish Manku 61311028e2 PageEnigma refactor Phase 4: build actions/ and rewire add/select/transform flows
Replaces the four-hop signal-stub pattern (drop -> store-only update,
engine never told) with explicit action helpers that mutate the engine
and the store in one step.

actions/:
- addObject / addCharacter / addShape: call editor.sceneManager.create
  to load the THREE.js object, then update the Zustand store. addCharacter
  also stamps userData.isCharacter on the loaded object
- deleteObject: thin wrapper over editor.deleteObject (which already
  goes through SceneUtils to update the store)
- selectObject / deselectObject: invoke sceneManager.select_object;
  Editor.publishSelect already writes the selection to the store
- setTransformMode: drives editor.change_mode + store + the legacy
  selectedMode signal during the migration window
- setCameraAspect: drives editor.changeRenderCameraAspectRatio + store +
  the legacy cameraAspectRatio signal
- index.ts barrel

Wiring updates:
- PageEditor drop handler (gallery -> 3D) now calls actions/addObject
  and actions/addCharacter with the editor and world position
- DragAndDrop/DndAsset takes the editor at pointerdown time and calls
  actions/* on drop. ItemElement passes the editor in via EngineContext
- AspectRatioMenu calls actions/setCameraAspect; PageEditor's bottom-bar
  aspect-ratio select uses the action too
- Controls3D's mode toggle (move/rotate/scale) goes through
  actions/setTransformMode and the four little redundant handlers are
  collapsed into one
- ImageTo3DExperience's "Open in 3D Editor" button uses getActiveEditor
  to pick up the live editor across pages

Plumbing:
- New module-level activeEditor handle in EngineContext (with
  getActiveEditor / setActiveEditor) so non-React callers and cross-page
  actions can reach the engine; EngineProvider sets it on init and
  clears on unmount
- Drop the now-orphaned signals/objectGroup, signals/characterGroups,
  signals/shape entirely. Their job (store update + engine call) lives
  in actions/. signals/index.ts loses the corresponding re-exports
- Remove the placeholder addLocalSplat onSuccess in Controls3D (a
  developer-test path for direct splat loading; the real splat upload
  goes through GlobalFileDropHandler) and delete the now-confirmed-dead
  recordScene method in editor.ts
- video_generation.ts zipBlob: type as Blob | null (matches the
  UploadStudioShot signature; pre-existing TS error)

Net effect on typecheck: 144 -> 147 errors in PageEnigma + experiences;
zero errors introduced in actions/, PageEnigmaStore, or DragAndDrop. All
new errors are pre-existing issues in adjacent files surfaced by the
edits, not regressions.
2026-04-28 22:37:30 -07:00
Tanish Manku 6d22d0d3c3 PageEnigma refactor: strip Queue, TimeLine, and timeline engines from editor.ts
editor.ts surgery (1864 -> 1665 lines):

- Drop imports + field declarations + constructor instantiations for
  AudioEngine, TransformEngine, EmotionEngine, LipSyncEngine,
  CharacterAnimationEngine, and TimeLine; drop the
  Queue/QueueNames/fromEngineActions/toEngineActions imports
- Drop timeline state fields: can_playback, playback_location,
  current_frame, last_scrub. generating_preview stays (it tracks the
  stylize preview render, not the timeline)
- Replace Queue.publish in changeRenderCameraAspectRatio, newScene,
  loadScene, and publishSelect with direct store actions
  (usePageEnigmaStore.getState().setSelectedObject / resetScene)
- Replace timeline.characters lookups (used by SceneManager outliner)
  with a getCharactersByUuid helper that reads the store
- Replace timeline.isCharacter (passed into SceneManager) with an
  isCharacterUuid helper that checks the store's character list
- recordScene becomes a no-op stub: timeline-driven frame capture is
  gone, video generation needs to drive its own frame loop. TODO marker
  for the rewiring
- generateVideo and togglePlayback drop their timeline.is_playing /
  scrubber gating; renderSingleFrame drops the timeline.update branch
- Bonus: kill the meraAspectRatio typo (a pre-existing bug in the
  recordScene branch I deleted)

Cascading fixes:

- save_manager.ts loadScene/loadCache: drop calls to the deleted
  timeline.scrub and timeline.checkEditorCanPlay
- video_generation.ts videoProcessingPipeline: stub the
  engineFrameBuffers.payloadZip path with a TODO; the video upload now
  always errors via the existing handleError. The full rewiring is the
  same TODO as recordScene
- Controls3D UploadModalSplat onSuccess: drop the
  editor.timeline.addLocalSplat call
- SidePanelTabs: drop a long-broken import of useMouseEventsSidePanel
  (the Timeline/utils path it pointed to was already deleted before
  this refactor); resize handle gets a TODO marker for Phase 6

Net effect on typecheck: 162 -> 144 errors in PageEnigma. Every
remaining error in PageEnigma predates this branch; no new TypeScript
errors introduced by the refactor.
2026-04-28 22:23:11 -07:00
Tanish Manku 50e68b0721 PageEnigma refactor: strip Queue and timeline-only signals from comps and engine helpers
Removes references to the deleted Queue/timeline subsystem from:

- Engine helpers: scene_manager_api, video_generation, helper. Their
  Queue.publish calls (UPDATE_CHARACTER_ID/ADD_OBJECT/POP_A_TOAST/
  DELETE_OBJECT) now write to the new Zustand store / addToast directly.
- Editor/EditorState: drop TimelineState class and its JSON variant; trim
  EditorState/EditorStateJson down to just sceneState and metadata.
- Editor/scene.ts: drop the timeline animation-clip seeding path; restore
  placeholder_manager (incorrectly classified as dead in the audit).
- Signals: collapse signals/objectGroup, signals/characterGroups, and
  signals/shape to non-timeline scaffolds that update the store directly
  with a TODO marker for the engine-side rewiring. Drop
  loadObjectData/loadCharacterData/updateCharacters/updateObjectGroup
  files (all timeline-only). Strip dndTimelineHeight + overTimeline from
  signals/dragAndDrop. Add setEditorState helper to signals/engine.
- Comps: drop Queue.publish calls. Aspect-ratio menu and stylize-tab
  state transitions go through signal setters; TOGGLE_CAMERA_STATE,
  REFRESH_PREVIEW, GENERATE_VIDEO are stubbed with TODOs pending the
  editor.ts rewiring. DragComponent simplified (timeline-drop branch was
  unreachable). PageEditor drops timelineHeight/dndTimelineHeight layout
  math.
- PageEnigma drops the useQueueHandler call.

The branch still does not compile: editor.ts (1864 lines) is the next
target -- it instantiates TimeLine + the five timeline engines and
publishes select/aspect-ratio events through the queue. Cleaning it up
is the focused work for the next commit.
2026-04-28 22:07:31 -07:00
Tanish Manku 1351a40b3d PageEnigma refactor checkpoint: delete timeline subsystem and add Zustand store
Phase 1-3 of the 3D page refactor:
- Delete the timeline + event-queue subsystem (Queue/, useQueueHandler,
  Editor/timeline.ts, Editor/Engines/, clips/, and the timeline-only
  signal slices: timeline, timing, audioGroup, cameraGroup,
  promptTravelGroup, utils/publishClip)
- Delete dead-code files with no live importers: lipsync.js,
  retargeting.ts, preview_video_generation.ts, shaders.js,
  placeholder_manager.ts, both composable_engines/*.tsx stubs,
  contexts/TrackContext/, comps/TimerGrid, comps/ControlsVideo
- Add PageEnigmaStore.ts with the full state shape that will replace
  preact-signals (scene objects, cameras, transform mode, layout, error
  overlays, generation/stylize state)
- Strip signals/index.ts of deleted slice exports and reduce
  signals/sizing.ts to just sidePanelWidth

Branch does not yet compile -- subsequent commits will strip
Queue/timeline references from editor.ts, the remaining signal files,
and component consumers, then build out actions/ and event hooks.
2026-04-28 21:56:58 -07:00
771 changed files with 16366 additions and 21478 deletions
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -116,9 +126,10 @@
true,
false,
true,
true,
false,
false
]
},
"hash": "fbe482c8987231c3b5d75f2e792d0fdd033de8a7eb821428fa034752c73edfb7"
"hash": "2c98621c08055374fd09502c94ac40bb01d23fcea3f844cceea7446a5c0cb4dc"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n maybe_debug_log_event_token = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 16
},
"nullable": []
},
"hash": "8f8b64c0af5383ff3baaa8aa2f2aa22cc760addd850d618ce543fe05816d11e0"
}
@@ -0,0 +1,80 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n id as `id: u64`,\n event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n message,\n created_at\nFROM debug_logs\nWHERE event_token = ?\nORDER BY id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 5,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
false,
false
]
},
"hash": "a62b88fec40a11952cc199383090ea338f97091e8bfc6b68aad67416685889af"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_creator_user_token = ?,\n message = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "a9745ecf60b564ab096ee1dbe124221b5f288a4ae38e788e19bbd359db0b580f"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 16
"Right": 17
},
"nullable": []
},
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
"hash": "c73be10c5677f78f81bc1c29872fd5af889790c3b2a8de27eb8ab64383c638d6"
}
@@ -0,0 +1,355 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n uuid_idempotency_token,\n job_type as `maybe_job_type`,\n product_category as `maybe_product_category`,\n inference_category,\n maybe_external_third_party,\n maybe_external_third_party_id,\n maybe_model_type,\n maybe_model_token,\n maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n maybe_inference_args,\n maybe_download_url,\n on_success_result_entity_type,\n on_success_result_entity_token,\n on_success_result_batch_token,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n maybe_creator_anonymous_visitor_token,\n creator_ip_address,\n creator_set_visibility,\n maybe_wallet_ledger_entry_token,\n maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n status,\n attempt_count as `attempt_count: u16`,\n failure_reason,\n internal_debugging_failure_reason,\n frontend_failure_category,\n maybe_routing_tag,\n assigned_worker,\n assigned_cluster,\n created_at,\n updated_at\nFROM generic_inference_jobs\nWHERE token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "uuid_idempotency_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 144
}
},
{
"ordinal": 2,
"name": "maybe_job_type",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_product_category",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "inference_category",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_external_third_party",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 6,
"name": "maybe_external_third_party_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 256
}
},
{
"ordinal": 7,
"name": "maybe_model_type",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_model_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_inference_args",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 262140
}
},
{
"ordinal": 11,
"name": "maybe_download_url",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 4096
}
},
{
"ordinal": 12,
"name": "on_success_result_entity_type",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "on_success_result_entity_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 14,
"name": "on_success_result_batch_token",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 15,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 16,
"name": "maybe_creator_anonymous_visitor_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 17,
"name": "creator_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 18,
"name": "creator_set_visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 19,
"name": "maybe_wallet_ledger_entry_token",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 20,
"name": "maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 21,
"name": "status",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
},
{
"ordinal": 22,
"name": "attempt_count: u16",
"type_info": {
"type": "Short",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 23,
"name": "failure_reason",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 24,
"name": "internal_debugging_failure_reason",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 25,
"name": "frontend_failure_category",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 26,
"name": "maybe_routing_tag",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 256
}
},
{
"ordinal": 27,
"name": "assigned_worker",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 512
}
},
{
"ordinal": 28,
"name": "assigned_cluster",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 512
}
},
{
"ordinal": 29,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 30,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "e869f065e6f184abbabae04ecd0da6ea02c0576cc22ee55e47eb91dd34048ef1"
}
Generated
+5
View File
@@ -11469,13 +11469,18 @@ name = "support-tool"
version = "0.0.1"
dependencies = [
"anyhow",
"artcraft_api_defs",
"artcraft_client",
"clap 4.5.60",
"easyenv",
"enums",
"errors",
"log",
"seedance2pro_client",
"serde_json",
"tokens",
"tokio",
"uuid 1.22.0",
]
[[package]]
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS debug_logs;
@@ -0,0 +1,31 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE debug_logs (
-- Not used for anything except replication.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Event token. Not unique — multiple log rows can share the same event.
event_token VARCHAR(32) NOT NULL,
-- The type of debug log entry.
debug_log_type VARCHAR(24) NOT NULL,
-- The user who created the log entry (if any).
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
-- The log message body.
message MEDIUMTEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_event_token (event_token),
KEY index_debug_log_type (debug_log_type),
KEY index_maybe_creator_user_token (maybe_creator_user_token),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN maybe_debug_log_event_token;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL;
@@ -0,0 +1,31 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE debug_logs (
-- Not used for anything except replication.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Event token. Not unique — multiple log rows can share the same event.
event_token VARCHAR(32) NOT NULL,
-- The type of debug log entry.
debug_log_type VARCHAR(24) NOT NULL,
-- The user who created the log entry (if any).
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
-- The log message body.
message MEDIUMTEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_event_token (event_token),
KEY index_debug_log_type (debug_log_type),
KEY index_maybe_creator_user_token (maybe_creator_user_token),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -204,6 +204,10 @@ CREATE TABLE generic_inference_jobs (
-- off special workers to handle certain jobs.
maybe_routing_tag VARCHAR(64) DEFAULT NULL,
-- Foreign key to the debug_logs table (event_token column).
-- Multiple debug_logs rows can share the same event token.
maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL,
-- ========== JOB SYSTEM DETAILS ==========
-- Jobs begin as "pending", then transition to other states.
@@ -1,5 +1,6 @@
use cloudflare_errors::cloudflare_error::CloudflareError;
use errors::AnyhowError;
use reqwest::StatusCode;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::io;
@@ -30,6 +31,12 @@ pub enum ApiError {
backend_hostname: Option<String>,
},
/// An uncategorized bad HTTP response with status code and body.
UncategorizedBadResponseWithStatusAndBody {
status_code: StatusCode,
body: String,
},
/// Cloudflare errors.
CloudflareError(CloudflareError),
@@ -66,6 +73,8 @@ impl Display for ApiError {
ApiError::TooManyRequests(msg) => write!(f, "Too many requests to Storyteller backend: {}", msg),
ApiError::InternalServerError {body, backend_hostname} =>
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
ApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body } =>
write!(f, "Uncategorized bad response with status code {} and body: {}", status_code, body),
// Server response handling errors
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
// Network errors
@@ -0,0 +1,157 @@
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use log::{info, warn};
use reqwest::Client;
use url::Url;
use tokens::tokens::media_files::MediaFileToken;
use crate::endpoints::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
pub struct DownloadMediaFileArgs<'a, P: AsRef<Path>> {
pub media_token: &'a MediaFileToken,
pub api_host: &'a ApiHost,
pub download_path: DownloadPath<P>,
}
pub enum DownloadPath<P: AsRef<Path>> {
/// Write the file to this exact path.
ExactFilename(P),
/// Write the file into this directory, generating a filename from the CDN URL extension.
Directory(P),
}
pub struct DownloadMediaFileResult {
/// The path the file was written to.
pub downloaded_file_path: PathBuf,
/// The full media file response from the API.
pub media_file_response: GetMediaFileSuccessResponse,
/// Size of downloaded file in bytes.
pub filesize_bytes: usize,
}
pub async fn download_media_file<P: AsRef<Path>>(
args: DownloadMediaFileArgs<'_, P>,
) -> Result<DownloadMediaFileResult, StorytellerError> {
let DownloadMediaFileArgs {
media_token,
api_host,
download_path
} = args;
// 1. Fetch media file info from the API.
let response = get_media_file(api_host, media_token).await?;
let media_class = &response.media_file.media_class;
let cdn_url = &response.media_file.media_links.cdn_url;
info!("Downloading media file {} of class {} from CDN: {}",
media_token.as_str(), media_class.to_str(), cdn_url);
// 2. Determine the output file path.
let output_path = match &download_path {
DownloadPath::ExactFilename(path) => path.as_ref().to_path_buf(),
DownloadPath::Directory(dir) => {
let filename = derive_filename_from_url(cdn_url, &media_token);
dir.as_ref().join(filename)
}
};
// 3. Download the bytes from the CDN.
let bytes = download_bytes(cdn_url).await?;
// 4. Write to disk.
let mut file = fs::File::create(&output_path)
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
file.write_all(&bytes)
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
file.flush()
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
info!("Downloaded {} bytes to {:?}", bytes.len(), output_path);
Ok(DownloadMediaFileResult {
downloaded_file_path: output_path,
media_file_response: response,
filesize_bytes: bytes.len(),
})
}
// ── Helpers ──
async fn download_bytes(url: &Url) -> Result<Vec<u8>, StorytellerError> {
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
let response = client.get(url.as_str())
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::OtherReqwestError(err)))?;
let status_code = response.status();
if !status_code.is_success() {
let body = response.text().await.unwrap_or_else(|err| {
warn!("Failed to retrieve response body: {}", err);
"".to_string()
});
return Err(StorytellerError::Api(ApiError::UncategorizedBadResponseWithStatusAndBody {
status_code,
body,
}));
}
let bytes = response.bytes()
.await
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
Ok(bytes.to_vec())
}
/// Derive a filename from the CDN URL's path extension, falling back to the media token.
fn derive_filename_from_url(url: &Url, media_token: &MediaFileToken) -> String {
let path = url.path();
let extension = Path::new(path)
.extension() // NB: without dot '.'
.and_then(|ext| ext.to_str())
.unwrap_or("bin");
format!("{}.{}", media_token.as_str(), extension)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_filename_from_url_extracts_extension() {
let url = Url::parse("https://cdn.example.com/files/abc123.png").unwrap();
let token = MediaFileToken::new_from_str("mf_test123");
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.png");
}
#[test]
fn derive_filename_from_url_handles_no_extension() {
let url = Url::parse("https://cdn.example.com/files/abc123").unwrap();
let token = MediaFileToken::new_from_str("mf_test123");
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.bin");
}
#[test]
fn derive_filename_from_url_handles_query_params() {
let url = Url::parse("https://cdn.example.com/files/abc123.mp4?token=xyz").unwrap();
let token = MediaFileToken::new_from_str("mf_test123");
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.mp4");
}
}
@@ -1 +1,2 @@
pub mod download_media_file;
pub mod upload_media_file_from_file;
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,44 +9,55 @@ use crate::generate::generate_image::generate_image_response::{
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::{
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs,
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs, Flux1DevEditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs,
enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs, Flux1DevRequest,
};
pub async fn execute_fal_flux_1_dev(
plan: &PlanFalFlux1Dev,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
let (webhook_response, outbound_request) = if let Some(image_url) = &plan.maybe_image_url {
// Image-to-image mode
let args = Flux1DevEditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = Flux1DevEditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_url: image_url.clone(),
num_images: plan.num_images.to_edit(),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1DevEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_flux_1_dev_edit_image_webhook(args)
let resp = enqueue_flux_1_dev_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Text-to-image mode
let args = Flux1DevArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = Flux1DevRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_t2i(),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1DevArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_flux_1_dev_text_to_image_webhook(args)
let resp = enqueue_flux_1_dev_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,44 +9,55 @@ use crate::generate::generate_image::generate_image_response::{
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs,
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs, Flux1SchnellEditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{
enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs,
enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs, Flux1SchnellRequest,
};
pub async fn execute_fal_flux_1_schnell(
plan: &PlanFalFlux1Schnell,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
let (webhook_response, outbound_request) = if let Some(image_url) = &plan.maybe_image_url {
// Image-to-image (redux) mode
let args = Flux1SchnellEditImageArgs {
let request = Flux1SchnellEditImageRequest {
image_url: image_url.clone(),
num_images: plan.num_images.to_edit(),
image_size: plan.edit_image_size,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1SchnellEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_flux_1_schnell_edit_image_webhook(args)
let resp = enqueue_flux_1_schnell_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Text-to-image mode
let args = Flux1SchnellArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = Flux1SchnellRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.t2i_aspect_ratio,
num_images: plan.num_images.to_t2i(),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1SchnellArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_flux_1_schnell_text_to_image_webhook(args)
let resp = enqueue_flux_1_schnell_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,17 +9,21 @@ use crate::generate::generate_image::generate_image_response::{
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args,
enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args, FluxPro11Request,
};
pub async fn execute_fal_flux_pro_1p1(
plan: &PlanFalFluxPro11,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = FluxPro11Args {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = FluxPro11Request {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_fal(),
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = FluxPro11Args {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
@@ -28,5 +35,6 @@ pub async fn execute_fal_flux_pro_1p1(
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,17 +9,21 @@ use crate::generate::generate_image::generate_image_response::{
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs,
enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs, FluxPro11UltraRequest,
};
pub async fn execute_fal_flux_pro_1p1_ultra(
plan: &PlanFalFluxPro11Ultra,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = FluxPro11UltraArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = FluxPro11UltraRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_fal(),
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = FluxPro11UltraArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
@@ -28,5 +35,6 @@ pub async fn execute_fal_flux_pro_1p1_ultra(
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -7,32 +10,39 @@ use crate::generate::generate_image::generate_image_response::{
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::PlanFalGptImage1;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{
enqueue_gpt_image_1_edit_image_webhook, EnqueueGptImage1EditImageArgs,
EnqueueGptImage1EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
enqueue_gpt_image_1_text_to_image_webhook, EnqueueGptImage1TextToImageArgs,
EnqueueGptImage1TextToImageRequest,
};
pub async fn execute_fal_gpt_image_1(
plan: &PlanFalGptImage1,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueGptImage1TextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueGptImage1TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: plan.num_images.to_t2i(),
image_size: plan.image_size.map(|s| s.to_t2i()),
quality: Some(plan.quality.to_t2i()),
background: None,
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1_text_to_image_webhook(args)
let resp = enqueue_gpt_image_1_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let args = EnqueueGptImage1EditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueGptImage1EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: plan.num_images.to_edit(),
mask_image_url: None,
@@ -41,17 +51,23 @@ pub async fn execute_fal_gpt_image_1(
input_fidelity: None,
background: None,
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1_edit_image_webhook(args)
let resp = enqueue_gpt_image_1_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -7,32 +10,39 @@ use crate::generate::generate_image::generate_image_response::{
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs,
EnqueueGptImage1p5EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs,
EnqueueGptImage1p5TextToImageRequest,
};
pub async fn execute_fal_gpt_image_1p5(
plan: &PlanFalGptImage1p5,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueGptImage1p5TextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueGptImage1p5TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: plan.num_images.to_t2i(),
image_size: plan.image_size.map(|s| s.to_t2i()),
background: None,
quality: Some(plan.quality.to_t2i()),
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1p5TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1p5_text_to_image_webhook(args)
let resp = enqueue_gpt_image_1p5_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let args = EnqueueGptImage1p5EditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueGptImage1p5EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: plan.num_images.to_edit(),
mask_image_url: None,
@@ -41,16 +51,22 @@ pub async fn execute_fal_gpt_image_1p5(
quality: Some(plan.quality.to_edit()),
input_fidelity: None,
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1p5EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1p5_image_edit_webhook(args)
let resp = enqueue_gpt_image_1p5_image_edit_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -7,48 +10,61 @@ use crate::generate::generate_image::generate_image_response::{
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::PlanFalGptImage2;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{
enqueue_gpt_image_2_edit_image_webhook, EnqueueGptImage2EditImageArgs,
EnqueueGptImage2EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_2_text_to_image_webhook::{
enqueue_gpt_image_2_text_to_image_webhook, EnqueueGptImage2TextToImageArgs,
EnqueueGptImage2TextToImageRequest,
};
pub async fn execute_fal_gpt_image_2(
plan: &PlanFalGptImage2,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueGptImage2TextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueGptImage2TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: plan.num_images.to_t2i(),
image_size: plan.image_size.map(|s| s.to_t2i()),
quality: Some(plan.quality.to_t2i()),
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage2TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_2_text_to_image_webhook(args)
let resp = enqueue_gpt_image_2_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let args = EnqueueGptImage2EditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueGptImage2EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: plan.num_images.to_edit(),
mask_url: None,
image_size: plan.image_size.map(|s| s.to_edit()),
quality: Some(plan.quality.to_edit()),
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage2EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_2_edit_image_webhook(args)
let resp = enqueue_gpt_image_2_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -9,47 +12,58 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
};
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{
enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs,
Gemini25FlashEditNumImages,
Gemini25FlashEditNumImages, Gemini25FlashEditRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{
enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs,
Gemini25FlashTextToImageNumImages,
Gemini25FlashTextToImageNumImages, Gemini25FlashTextToImageRequest,
};
pub async fn execute_fal_nano_banana(
plan: &PlanFalNanoBanana,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
// Text-to-image mode
let args = Gemini25FlashTextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = Gemini25FlashTextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: to_t2i_num_images(plan.num_images),
aspect_ratio: plan.t2i_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Gemini25FlashTextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gemini_25_flash_text_to_image_webhook(args)
let resp = enqueue_gemini_25_flash_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Image-edit mode
let args = Gemini25FlashEditArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = Gemini25FlashEditRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
aspect_ratio: plan.edit_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Gemini25FlashEditArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gemini_25_flash_edit_webhook(args)
let resp = enqueue_gemini_25_flash_edit_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -9,49 +12,62 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
};
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{
enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs,
EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageResolution,
EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageRequest,
EnqueueNanoBanana2EditImageResolution,
};
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{
enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs,
EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageResolution,
EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageRequest,
EnqueueNanoBanana2TextToImageResolution,
};
pub async fn execute_fal_nano_banana_2(
plan: &PlanFalNanaBanana2,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
// Text-to-image mode
let args = EnqueueNanoBanana2TextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueNanoBanana2TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: to_t2i_num_images(plan.num_images),
resolution: plan.resolution.map(to_t2i_resolution),
aspect_ratio: plan.t2i_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBanana2TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_nano_banana_2_text_to_image_webhook(args)
let resp = enqueue_nano_banana_2_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Image-edit mode
let args = EnqueueNanoBanana2EditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueNanoBanana2EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
resolution: plan.resolution.map(to_edit_resolution),
aspect_ratio: plan.edit_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBanana2EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_nano_banana_2_edit_image_webhook(args)
let resp = enqueue_nano_banana_2_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -9,48 +12,61 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
};
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{
enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs,
EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution,
EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageRequest,
EnqueueNanoBananaProEditImageResolution,
};
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{
enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs,
EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution,
EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageRequest,
EnqueueNanoBananaProTextToImageResolution,
};
pub async fn execute_fal_nano_banana_pro(
plan: &PlanFalNanaBananaPro,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
// Text-to-image mode
let args = EnqueueNanoBananaProTextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueNanoBananaProTextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: to_t2i_num_images(plan.num_images),
resolution: plan.resolution.map(to_t2i_resolution),
aspect_ratio: plan.t2i_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBananaProTextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_nano_banana_pro_text_to_image_webhook(args)
let resp = enqueue_nano_banana_pro_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Image-edit mode
let args = EnqueueNanoBananaProEditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueNanoBananaProEditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
resolution: plan.resolution.map(to_edit_resolution),
aspect_ratio: plan.edit_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBananaProEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_nano_banana_pro_image_edit_webhook(args)
let resp = enqueue_nano_banana_pro_image_edit_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -7,44 +10,57 @@ use crate::generate::generate_image::generate_image_response::{
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{
enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs,
EnqueueBytedanceSeedreamV4EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs,
EnqueueBytedanceSeedreamV4TextToImageRequest,
};
pub async fn execute_fal_seedream_4(
plan: &PlanFalSeedream4,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueBytedanceSeedreamV4TextToImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
let resp = enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueBytedanceSeedreamV4EditImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
image_urls: plan.image_urls.clone(),
num_images: Some(plan.num_images.to_edit()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_edit()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4_edit_image_webhook(args)
let resp = enqueue_bytedance_seedream_v4_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -7,44 +10,57 @@ use crate::generate::generate_image::generate_image_response::{
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{
enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs,
EnqueueBytedanceSeedreamV4p5EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs,
EnqueueBytedanceSeedreamV4p5TextToImageRequest,
};
pub async fn execute_fal_seedream_4p5(
plan: &PlanFalSeedream4p5,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueBytedanceSeedreamV4p5TextToImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
let resp = enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueBytedanceSeedreamV4p5EditImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
image_urls: plan.image_urls.clone(),
num_images: Some(plan.num_images.to_edit()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_edit()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4p5_edit_image_webhook(args)
let resp = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -7,44 +10,57 @@ use crate::generate::generate_image::generate_image_response::{
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{
enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs,
EnqueueBytedanceSeedreamV5LiteEditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs,
EnqueueBytedanceSeedreamV5LiteTextToImageRequest,
};
pub async fn execute_fal_seedream_5_lite(
plan: &PlanFalSeedream5Lite,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueBytedanceSeedreamV5LiteTextToImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args)
let resp = enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
prompt: plan.prompt.as_deref().unwrap_or(""),
let request = EnqueueBytedanceSeedreamV5LiteEditImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
image_urls: plan.image_urls.clone(),
num_images: Some(plan.num_images.to_edit()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_edit()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args)
let resp = enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug)]
@@ -9,6 +12,11 @@ pub struct ArtcraftImageResponsePayload {
pub struct FalImageResponsePayload {
pub request_id: Option<String>,
pub gateway_request_id: Option<String>,
/// The outbound request that was sent to Fal.
/// Stored as a trait object so any Request type can be captured.
/// Use `format!("{:?}", ...)` or `format!("{:#?}", ...)` to print.
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
#[derive(Clone, Debug)]
@@ -1,7 +1,7 @@
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::Kling3p0ProMultiFunctionVideoGenDuration;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::{
EnqueueKling3p0ProTextToVideoArgs,
EnqueueKling3p0ProTextToVideoRequest,
EnqueueKling3p0ProTextToVideoDuration,
};
@@ -29,19 +29,16 @@ pub(crate) fn estimate_video_cost_artcraft_kling3p0_pro(
});
// Use text-to-video cost calculator (same pricing for both modes)
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueKling3p0ProTextToVideoArgs {
let req = EnqueueKling3p0ProTextToVideoRequest {
prompt: String::new(),
generate_audio: plan.generate_audio,
negative_prompt: None,
duration,
aspect_ratio: None,
shot_type: None,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -1,7 +1,7 @@
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::Kling3p0StandardMultiFunctionVideoGenDuration;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::{
EnqueueKling3p0StandardTextToVideoArgs,
EnqueueKling3p0StandardTextToVideoRequest,
EnqueueKling3p0StandardTextToVideoDuration,
};
@@ -29,19 +29,16 @@ pub(crate) fn estimate_video_cost_artcraft_kling3p0_standard(
});
// Use text-to-video cost calculator (same pricing for both modes)
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueKling3p0StandardTextToVideoArgs {
let req = EnqueueKling3p0StandardTextToVideoRequest {
prompt: String::new(),
generate_audio: plan.generate_audio,
negative_prompt: None,
duration,
aspect_ratio: None,
shot_type: None,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -1,7 +1,7 @@
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::Seedance1p5ProMultiFunctionVideoGenDuration;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
EnqueueSeedance1p5ProTextToVideoArgs,
EnqueueSeedance1p5ProTextToVideoRequest,
EnqueueSeedance1p5ProTextToVideoDuration,
EnqueueSeedance1p5ProTextToVideoResolution,
};
@@ -33,18 +33,15 @@ pub(crate) fn estimate_video_cost_artcraft_seedance1p5_pro(
});
// Use text-to-video cost calculator (same pricing for both modes)
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueSeedance1p5ProTextToVideoArgs {
let req = EnqueueSeedance1p5ProTextToVideoRequest {
prompt: String::new(),
duration,
resolution,
aspect_ratio: None,
generate_audio: plan.generate_audio,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -1,8 +1,7 @@
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{
Seedance1LiteArgs, Seedance1LiteDuration, Seedance1LiteResolution,
Seedance1LiteDuration, Seedance1LiteRequest, Seedance1LiteResolution,
};
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance_1_0_lite::PlanArtcraftSeedance10Lite;
@@ -13,7 +12,6 @@ pub(crate) fn estimate_video_cost_artcraft_seedance_1_0_lite(
) -> VideoGenerationCostEstimate {
// Legacy handler defaults: 5 seconds, 720p. Delegate to the Fal client's
// cost calculator to guarantee parity with what we actually charge.
let api_key = FalApiKey::from_str("");
let duration = if plan.duration_seconds_for_cost() == 10 {
Seedance1LiteDuration::TenSeconds
} else {
@@ -24,21 +22,18 @@ pub(crate) fn estimate_video_cost_artcraft_seedance_1_0_lite(
GenerateSeedance10LiteResolution::SevenTwentyP => Seedance1LiteResolution::SevenTwentyP,
};
let end_frame: Option<&str> = None;
let args = Seedance1LiteArgs {
image_url: "",
end_frame_image_url: end_frame,
prompt: "",
let req = Seedance1LiteRequest {
image_url: String::new(),
end_frame_image_url: None,
prompt: String::new(),
duration,
resolution,
aspect_ratio: None,
camera_fixed: false,
seed: None,
api_key: &api_key,
webhook_url: "https://example.com",
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -1,6 +1,5 @@
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::Seedance1LiteArgs;
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::Seedance1LiteRequest;
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_seedance_1_0_lite::PlanFalSeedance10Lite;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
@@ -9,22 +8,18 @@ pub(crate) fn estimate_video_cost_fal_seedance_1_0_lite(
plan: &PlanFalSeedance10Lite,
) -> VideoGenerationCostEstimate {
// Delegate to the Fal client's cost calculator to guarantee parity with billing.
let api_key = FalApiKey::from_str("");
let end_frame: Option<&str> = plan.end_image_url.as_deref();
let args = Seedance1LiteArgs {
image_url: plan.image_url.as_str(),
end_frame_image_url: end_frame,
prompt: plan.prompt.as_str(),
let req = Seedance1LiteRequest {
image_url: plan.image_url.clone(),
end_frame_image_url: plan.end_image_url.clone(),
prompt: plan.prompt.clone(),
duration: plan.duration,
resolution: plan.resolution,
aspect_ratio: plan.aspect_ratio,
camera_fixed: false,
seed: None,
api_key: &api_key,
webhook_url: "https://example.com",
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -1,7 +1,6 @@
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
EnqueueSeedance1p5ProTextToVideoArgs, EnqueueSeedance1p5ProTextToVideoDuration,
EnqueueSeedance1p5ProTextToVideoRequest, EnqueueSeedance1p5ProTextToVideoDuration,
EnqueueSeedance1p5ProTextToVideoResolution,
};
@@ -15,18 +14,15 @@ pub(crate) fn estimate_video_cost_fal_seedance_1p5_pro(
) -> VideoGenerationCostEstimate {
// Seedance 1.5 Pro t2v and i2v have identical pricing; delegate to the t2v
// calculator to guarantee parity with billing.
let api_key = FalApiKey::from_str("");
let args = EnqueueSeedance1p5ProTextToVideoArgs {
let req = EnqueueSeedance1p5ProTextToVideoRequest {
prompt: String::new(),
resolution: plan.resolution.map(to_t2v_resolution),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: None,
generate_audio: plan.generate_audio,
webhook_url: "https://example.com",
api_key: &api_key,
};
let cost_in_usd_cents = args.calculate_cost_in_cents();
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,26 +9,28 @@ use crate::generate::generate_video::generate_video_response::{
};
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::PlanFalKling16Pro;
use fal_client::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{
enqueue_kling_v1p6_pro_image_to_video_webhook, Kling1p6ProArgs,
enqueue_kling_v1p6_pro_image_to_video_webhook, Kling1p6ProArgs, Kling1p6ProRequest,
};
pub async fn execute_fal_kling_1_6_pro(
plan: &PlanFalKling16Pro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
// The Fal arg type is generic over `Option<T: IntoUrl>` for the end frame URL —
// explicitly bind T to String here.
let end_frame_image_url: Option<String> = plan.end_image_url.clone();
let args = Kling1p6ProArgs {
image_url: plan.image_url.as_str(),
end_frame_image_url,
webhook_url: fal_client.webhook_url.as_str(),
prompt: plan.prompt.as_str(),
api_key: &fal_client.api_key,
let request = Kling1p6ProRequest {
image_url: plan.image_url.clone(),
end_frame_image_url: plan.end_image_url.clone(),
prompt: plan.prompt.clone(),
duration: plan.duration,
aspect_ratio: plan.aspect_ratio,
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Kling1p6ProArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let webhook_response = enqueue_kling_v1p6_pro_image_to_video_webhook(args)
.await
@@ -34,5 +39,6 @@ pub async fn execute_fal_kling_1_6_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,21 +9,27 @@ use crate::generate::generate_video::generate_video_response::{
};
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_2_1_master::PlanFalKling21Master;
use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{
enqueue_kling_v2p1_master_image_to_video_webhook, Kling2p1MasterArgs,
enqueue_kling_v2p1_master_image_to_video_webhook, Kling2p1MasterArgs, Kling2p1MasterRequest,
};
pub async fn execute_fal_kling_2_1_master(
plan: &PlanFalKling21Master,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let args = Kling2p1MasterArgs {
image_url: plan.image_url.as_str(),
webhook_url: fal_client.webhook_url.as_str(),
prompt: plan.prompt.as_str(),
api_key: &fal_client.api_key,
let request = Kling2p1MasterRequest {
image_url: plan.image_url.clone(),
prompt: plan.prompt.clone(),
duration: plan.duration,
aspect_ratio: plan.aspect_ratio,
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Kling2p1MasterArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let webhook_response = enqueue_kling_v2p1_master_image_to_video_webhook(args)
.await
@@ -29,5 +38,6 @@ pub async fn execute_fal_kling_2_1_master(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,25 +9,29 @@ use crate::generate::generate_video::generate_video_response::{
};
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_2_1_pro::PlanFalKling21Pro;
use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{
enqueue_kling_v2p1_pro_image_to_video_webhook, Kling2p1ProArgs,
enqueue_kling_v2p1_pro_image_to_video_webhook, Kling2p1ProArgs, Kling2p1ProRequest,
};
pub async fn execute_fal_kling_2_1_pro(
plan: &PlanFalKling21Pro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let end_frame_image_url: Option<String> = plan.end_image_url.clone();
let args = Kling2p1ProArgs {
image_url: plan.image_url.as_str(),
end_frame_image_url,
webhook_url: fal_client.webhook_url.as_str(),
prompt: plan.prompt.as_str(),
api_key: &fal_client.api_key,
let request = Kling2p1ProRequest {
image_url: plan.image_url.clone(),
end_frame_image_url: plan.end_image_url.clone(),
prompt: plan.prompt.clone(),
duration: plan.duration,
aspect_ratio: plan.aspect_ratio,
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Kling2p1ProArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let webhook_response = enqueue_kling_v2p1_pro_image_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
@@ -32,5 +39,6 @@ pub async fn execute_fal_kling_2_1_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -10,40 +13,49 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_2_
};
use fal_client::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{
enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs,
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds,
EnqueueKlingV2p5TurboProImageToVideoRequest, EnqueueKlingV2p5TurboProImageToVideoDurationSeconds,
};
use fal_client::requests::webhook::video::text::enqueue_kling_v2p5_turbo_pro_text_to_video_webhook::{
enqueue_kling_v2p5_turbo_pro_text_to_video_webhook, EnqueueKlingV2p5TurboProTextToVideoArgs,
EnqueueKlingV2p5TurboProTextToVideoAspectRatio, EnqueueKlingV2p5TurboProTextToVideoDurationSeconds,
EnqueueKlingV2p5TurboProTextToVideoRequest, EnqueueKlingV2p5TurboProTextToVideoAspectRatio,
EnqueueKlingV2p5TurboProTextToVideoDurationSeconds,
};
pub async fn execute_fal_kling_2_5_turbo_pro(
plan: &PlanFalKling2p5TurboPro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalKling2p5TurboProMode::TextToVideo => {
let args = EnqueueKlingV2p5TurboProTextToVideoArgs {
let request = EnqueueKlingV2p5TurboProTextToVideoRequest {
prompt: plan.prompt.clone(),
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKlingV2p5TurboProTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_v2p5_turbo_pro_text_to_video_webhook(args).await
(enqueue_kling_v2p5_turbo_pro_text_to_video_webhook(args).await, outbound)
}
FalKling2p5TurboProMode::ImageToVideo { image_url, end_image_url } => {
let args = EnqueueKlingV2p5TurboProImageToVideoArgs {
let request = EnqueueKlingV2p5TurboProImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
tail_image_url: end_image_url.clone(),
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration.map(to_i2v_duration),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKlingV2p5TurboProImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_v2p5_turbo_pro_image_to_video_webhook(args).await
(enqueue_kling_v2p5_turbo_pro_image_to_video_webhook(args).await, outbound)
}
};
@@ -53,6 +65,7 @@ pub async fn execute_fal_kling_2_5_turbo_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -9,41 +12,50 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_2_
};
use fal_client::requests::webhook::video::image::enqueue_kling_v2p6_pro_image_to_video_webhook::{
enqueue_kling_v2p6_pro_image_to_video_webhook, EnqueueKlingV2p6ProImageToVideoArgs,
EnqueueKlingV2p6ProImageToVideoDurationSeconds,
EnqueueKlingV2p6ProImageToVideoRequest, EnqueueKlingV2p6ProImageToVideoDurationSeconds,
};
use fal_client::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{
enqueue_kling_v2p6_pro_text_to_video_webhook, EnqueueKlingV2p6ProTextToVideoArgs,
EnqueueKlingV2p6ProTextToVideoAspectRatio, EnqueueKlingV2p6ProTextToVideoDurationSeconds,
EnqueueKlingV2p6ProTextToVideoRequest, EnqueueKlingV2p6ProTextToVideoAspectRatio,
EnqueueKlingV2p6ProTextToVideoDurationSeconds,
};
pub async fn execute_fal_kling_2_6_pro(
plan: &PlanFalKling2p6Pro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalKling2p6ProMode::TextToVideo => {
let args = EnqueueKlingV2p6ProTextToVideoArgs {
let request = EnqueueKlingV2p6ProTextToVideoRequest {
prompt: plan.prompt.clone(),
generate_audio: plan.generate_audio,
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKlingV2p6ProTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_v2p6_pro_text_to_video_webhook(args).await
(enqueue_kling_v2p6_pro_text_to_video_webhook(args).await, outbound)
}
FalKling2p6ProMode::ImageToVideo { image_url } => {
let args = EnqueueKlingV2p6ProImageToVideoArgs {
let request = EnqueueKlingV2p6ProImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
generate_audio: plan.generate_audio,
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration.map(to_i2v_duration),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKlingV2p6ProImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_v2p6_pro_image_to_video_webhook(args).await
(enqueue_kling_v2p6_pro_image_to_video_webhook(args).await, outbound)
}
};
@@ -53,6 +65,7 @@ pub async fn execute_fal_kling_2_6_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -9,33 +12,39 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_3p
};
use fal_client::requests::webhook::video::image::enqueue_kling_3p0_pro_image_to_video_webhook::{
enqueue_kling_3p0_pro_image_to_video_webhook, EnqueueKling3p0ProImageToVideoArgs,
EnqueueKling3p0ProImageToVideoAspectRatio, EnqueueKling3p0ProImageToVideoDuration,
EnqueueKling3p0ProImageToVideoRequest, EnqueueKling3p0ProImageToVideoAspectRatio,
EnqueueKling3p0ProImageToVideoDuration,
};
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_pro_text_to_video_webhook::{
enqueue_kling_3p0_pro_text_to_video_webhook, EnqueueKling3p0ProTextToVideoArgs,
EnqueueKling3p0ProTextToVideoAspectRatio, EnqueueKling3p0ProTextToVideoDuration,
EnqueueKling3p0ProTextToVideoRequest, EnqueueKling3p0ProTextToVideoAspectRatio,
EnqueueKling3p0ProTextToVideoDuration,
};
pub async fn execute_fal_kling_3p0_pro(
plan: &PlanFalKling3p0Pro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalKling3p0Mode::TextToVideo => {
let args = EnqueueKling3p0ProTextToVideoArgs {
let request = EnqueueKling3p0ProTextToVideoRequest {
prompt: plan.prompt.clone(),
generate_audio: plan.generate_audio,
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio),
shot_type: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKling3p0ProTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_3p0_pro_text_to_video_webhook(args).await
(enqueue_kling_3p0_pro_text_to_video_webhook(args).await, outbound)
}
FalKling3p0Mode::ImageToVideo { image_url, end_image_url } => {
let args = EnqueueKling3p0ProImageToVideoArgs {
let request = EnqueueKling3p0ProImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
end_image_url: end_image_url.clone(),
@@ -44,10 +53,14 @@ pub async fn execute_fal_kling_3p0_pro(
duration: plan.duration.map(to_i2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio),
shot_type: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKling3p0ProImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_3p0_pro_image_to_video_webhook(args).await
(enqueue_kling_3p0_pro_image_to_video_webhook(args).await, outbound)
}
};
@@ -57,6 +70,7 @@ pub async fn execute_fal_kling_3p0_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -10,33 +13,39 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_3p
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_3p0_standard::PlanFalKling3p0Standard;
use fal_client::requests::webhook::video::image::enqueue_kling_3p0_standard_image_to_video_webhook::{
enqueue_kling_3p0_standard_image_to_video_webhook, EnqueueKling3p0StandardImageToVideoArgs,
EnqueueKling3p0StandardImageToVideoAspectRatio, EnqueueKling3p0StandardImageToVideoDuration,
EnqueueKling3p0StandardImageToVideoRequest, EnqueueKling3p0StandardImageToVideoAspectRatio,
EnqueueKling3p0StandardImageToVideoDuration,
};
use fal_client::requests::webhook::video::text::enqueue_kling_3p0_standard_text_to_video_webhook::{
enqueue_kling_3p0_standard_text_to_video_webhook, EnqueueKling3p0StandardTextToVideoArgs,
EnqueueKling3p0StandardTextToVideoAspectRatio, EnqueueKling3p0StandardTextToVideoDuration,
EnqueueKling3p0StandardTextToVideoRequest, EnqueueKling3p0StandardTextToVideoAspectRatio,
EnqueueKling3p0StandardTextToVideoDuration,
};
pub async fn execute_fal_kling_3p0_standard(
plan: &PlanFalKling3p0Standard,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalKling3p0Mode::TextToVideo => {
let args = EnqueueKling3p0StandardTextToVideoArgs {
let request = EnqueueKling3p0StandardTextToVideoRequest {
prompt: plan.prompt.clone(),
generate_audio: plan.generate_audio,
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio),
shot_type: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKling3p0StandardTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_3p0_standard_text_to_video_webhook(args).await
(enqueue_kling_3p0_standard_text_to_video_webhook(args).await, outbound)
}
FalKling3p0Mode::ImageToVideo { image_url, end_image_url } => {
let args = EnqueueKling3p0StandardImageToVideoArgs {
let request = EnqueueKling3p0StandardImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
end_image_url: end_image_url.clone(),
@@ -45,10 +54,14 @@ pub async fn execute_fal_kling_3p0_standard(
duration: plan.duration.map(to_i2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio),
shot_type: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueKling3p0StandardImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_kling_3p0_standard_image_to_video_webhook(args).await
(enqueue_kling_3p0_standard_image_to_video_webhook(args).await, outbound)
}
};
@@ -58,6 +71,7 @@ pub async fn execute_fal_kling_3p0_standard(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,24 +9,28 @@ use crate::generate::generate_video::generate_video_response::{
};
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_seedance_1_0_lite::PlanFalSeedance10Lite;
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{
enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs,
enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteRequest,
};
pub async fn execute_fal_seedance_1_0_lite(
plan: &PlanFalSeedance10Lite,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let end_frame: Option<&str> = plan.end_image_url.as_deref();
let args = Seedance1LiteArgs {
image_url: plan.image_url.as_str(),
end_frame_image_url: end_frame,
prompt: plan.prompt.as_str(),
let request = Seedance1LiteRequest {
image_url: plan.image_url.clone(),
end_frame_image_url: plan.end_image_url.clone(),
prompt: plan.prompt.clone(),
duration: plan.duration,
resolution: plan.resolution,
aspect_ratio: plan.aspect_ratio,
camera_fixed: false,
seed: None,
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Seedance1LiteArgs {
request,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
@@ -35,5 +42,6 @@ pub async fn execute_fal_seedance_1_0_lite(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -11,33 +14,37 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_seedance
use fal_client::requests::webhook::video::image::enqueue_seedance_1p5_pro_image_to_video_webhook::{
enqueue_seedance_1p5_pro_image_to_video_webhook, EnqueueSeedance1p5ProImageToVideoArgs,
EnqueueSeedance1p5ProImageToVideoAspectRatio, EnqueueSeedance1p5ProImageToVideoDuration,
EnqueueSeedance1p5ProImageToVideoResolution,
EnqueueSeedance1p5ProImageToVideoRequest, EnqueueSeedance1p5ProImageToVideoResolution,
};
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
enqueue_seedance_1p5_pro_text_to_video_webhook, EnqueueSeedance1p5ProTextToVideoArgs,
EnqueueSeedance1p5ProTextToVideoAspectRatio, EnqueueSeedance1p5ProTextToVideoDuration,
EnqueueSeedance1p5ProTextToVideoResolution,
EnqueueSeedance1p5ProTextToVideoRequest, EnqueueSeedance1p5ProTextToVideoAspectRatio,
EnqueueSeedance1p5ProTextToVideoDuration, EnqueueSeedance1p5ProTextToVideoResolution,
};
pub async fn execute_fal_seedance_1p5_pro(
plan: &PlanFalSeedance1p5Pro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalSeedance1p5ProMode::TextToVideo => {
let args = EnqueueSeedance1p5ProTextToVideoArgs {
let request = EnqueueSeedance1p5ProTextToVideoRequest {
prompt: plan.prompt.clone(),
resolution: plan.resolution.map(to_t2v_resolution),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio),
generate_audio: plan.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueSeedance1p5ProTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_seedance_1p5_pro_text_to_video_webhook(args).await
(enqueue_seedance_1p5_pro_text_to_video_webhook(args).await, outbound)
}
FalSeedance1p5ProMode::ImageToVideo { image_url, end_image_url } => {
let args = EnqueueSeedance1p5ProImageToVideoArgs {
let request = EnqueueSeedance1p5ProImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
end_image_url: end_image_url.clone(),
@@ -45,10 +52,14 @@ pub async fn execute_fal_seedance_1p5_pro(
duration: plan.duration.map(to_i2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio),
generate_audio: plan.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueSeedance1p5ProImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_seedance_1p5_pro_image_to_video_webhook(args).await
(enqueue_seedance_1p5_pro_image_to_video_webhook(args).await, outbound)
}
};
@@ -58,6 +69,7 @@ pub async fn execute_fal_seedance_1p5_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -10,42 +13,50 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_sora_2::
use fal_client::requests::webhook::video::image::enqueue_sora_2_image_to_video_webhook::{
enqueue_sora_2_image_to_video_webhook, EnqueueSora2ImageToVideoArgs,
EnqueueSora2ImageToVideoAspectRatio, EnqueueSora2ImageToVideoDurationSeconds,
EnqueueSora2ImageToVideoResolution,
EnqueueSora2ImageToVideoRequest, EnqueueSora2ImageToVideoResolution,
};
use fal_client::requests::webhook::video::text::enqueue_sora_2_text_to_video_webhook::{
enqueue_sora_2_text_to_video_webhook, EnqueueSora2TextToVideoArgs,
EnqueueSora2TextToVideoAspectRatio, EnqueueSora2TextToVideoDurationSeconds,
EnqueueSora2TextToVideoResolution,
EnqueueSora2TextToVideoRequest, EnqueueSora2TextToVideoAspectRatio,
EnqueueSora2TextToVideoDurationSeconds, EnqueueSora2TextToVideoResolution,
};
pub async fn execute_fal_sora_2(
plan: &PlanFalSora2,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalSora2Mode::TextToVideo => {
// Text-to-video does not support Auto aspect ratio or Auto resolution.
let args = EnqueueSora2TextToVideoArgs {
let request = EnqueueSora2TextToVideoRequest {
prompt: plan.prompt.clone(),
resolution: plan.resolution.and_then(to_t2v_resolution),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.and_then(to_t2v_aspect_ratio),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueSora2TextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_sora_2_text_to_video_webhook(args).await
(enqueue_sora_2_text_to_video_webhook(args).await, outbound)
}
FalSora2Mode::ImageToVideo { image_url } => {
let args = EnqueueSora2ImageToVideoArgs {
let request = EnqueueSora2ImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
duration: plan.duration.map(to_i2v_duration),
resolution: plan.resolution.map(to_i2v_resolution),
aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueSora2ImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_sora_2_image_to_video_webhook(args).await
(enqueue_sora_2_image_to_video_webhook(args).await, outbound)
}
};
@@ -55,6 +66,7 @@ pub async fn execute_fal_sora_2(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -11,42 +14,50 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_sora_2_p
use fal_client::requests::webhook::video::image::enqueue_sora_2_pro_image_to_video_webhook::{
enqueue_sora_2_pro_image_to_video_webhook, EnqueueSora2ProImageToVideoArgs,
EnqueueSora2ProImageToVideoAspectRatio, EnqueueSora2ProImageToVideoDurationSeconds,
EnqueueSora2ProImageToVideoResolution,
EnqueueSora2ProImageToVideoRequest, EnqueueSora2ProImageToVideoResolution,
};
use fal_client::requests::webhook::video::text::enqueue_sora_2_pro_text_to_video_webhook::{
enqueue_sora_2_pro_text_to_video_webhook, EnqueueSora2ProTextToVideoArgs,
EnqueueSora2ProTextToVideoAspectRatio, EnqueueSora2ProTextToVideoDurationSeconds,
EnqueueSora2ProTextToVideoResolution,
EnqueueSora2ProTextToVideoRequest, EnqueueSora2ProTextToVideoAspectRatio,
EnqueueSora2ProTextToVideoDurationSeconds, EnqueueSora2ProTextToVideoResolution,
};
pub async fn execute_fal_sora_2_pro(
plan: &PlanFalSora2Pro,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalSora2ProMode::TextToVideo => {
// Text-to-video does not support Auto aspect ratio or Auto resolution.
let args = EnqueueSora2ProTextToVideoArgs {
let request = EnqueueSora2ProTextToVideoRequest {
prompt: plan.prompt.clone(),
resolution: plan.resolution.and_then(to_t2v_resolution),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.and_then(to_t2v_aspect_ratio),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueSora2ProTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_sora_2_pro_text_to_video_webhook(args).await
(enqueue_sora_2_pro_text_to_video_webhook(args).await, outbound)
}
FalSora2ProMode::ImageToVideo { image_url } => {
let args = EnqueueSora2ProImageToVideoArgs {
let request = EnqueueSora2ProImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: image_url.clone(),
duration: plan.duration.map(to_i2v_duration),
resolution: plan.resolution.map(to_i2v_resolution),
aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueSora2ProImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_sora_2_pro_image_to_video_webhook(args).await
(enqueue_sora_2_pro_image_to_video_webhook(args).await, outbound)
}
};
@@ -56,6 +67,7 @@ pub async fn execute_fal_sora_2_pro(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -8,48 +11,59 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::{
FalVeo2Mode, PlanFalVeo2,
};
use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio,
enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Request,
};
use fal_client::requests::webhook::video::text::enqueue_veo_2_text_to_video_webhook::{
enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs,
enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs, Veo2TextToVideoRequest,
};
pub async fn execute_fal_veo_2(
plan: &PlanFalVeo2,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalVeo2Mode::TextToVideo => {
let args = Veo2TextToVideoArgs {
prompt: plan.prompt.as_str(),
negative_prompt: plan.negative_prompt.as_deref(),
api_key: &fal_client.api_key,
let request = Veo2TextToVideoRequest {
prompt: plan.prompt.clone(),
negative_prompt: plan.negative_prompt.clone(),
duration: plan.duration,
aspect_ratio: plan.aspect_ratio.unwrap_or(Veo2AspectRatio::Auto),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Veo2TextToVideoArgs {
request,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
enqueue_veo_2_text_to_video_webhook(args)
let resp = enqueue_veo_2_text_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
}
FalVeo2Mode::ImageToVideo { image_url } => {
// Image-to-video does not support aspect_ratio — the output
// inherits the source image's aspect ratio.
let args = Veo2Args {
image_url: image_url.as_str(),
prompt: plan.prompt.as_str(),
api_key: &fal_client.api_key,
let request = Veo2Request {
image_url: image_url.to_string(),
prompt: plan.prompt.clone(),
duration: plan.duration,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Veo2Args {
request,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
enqueue_veo_2_image_to_video_webhook(args)
let resp = enqueue_veo_2_image_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
}
};
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -10,13 +13,67 @@ use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::{
};
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration,
Veo3I2vResolution,
Veo3I2vResolution, Veo3Request,
};
use fal_client::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
enqueue_veo_3_text_to_video_webhook, Veo3T2vAspectRatio, Veo3T2vDuration, Veo3T2vResolution,
Veo3TextToVideoArgs,
Veo3TextToVideoArgs, Veo3TextToVideoRequest,
};
pub async fn execute_fal_veo_3(
plan: &PlanFalVeo3,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = match &plan.mode {
FalVeo3Mode::TextToVideo => {
let request = Veo3TextToVideoRequest {
prompt: plan.prompt.clone(),
negative_prompt: plan.negative_prompt.clone(),
duration: to_t2v_duration(plan.duration),
aspect_ratio: plan.t2v_aspect_ratio.map(to_t2v_aspect_ratio).unwrap_or(Veo3T2vAspectRatio::Default),
resolution: to_t2v_resolution(plan.resolution),
generate_audio: plan.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Veo3TextToVideoArgs {
request,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
let resp = enqueue_veo_3_text_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
}
FalVeo3Mode::ImageToVideo { image_url } => {
let request = Veo3Request {
image_url: image_url.to_string(),
prompt: plan.prompt.clone(),
duration: to_i2v_duration(plan.duration),
aspect_ratio: plan.i2v_aspect_ratio.map(to_i2v_aspect_ratio).unwrap_or(Veo3I2vAspectRatio::Auto),
resolution: to_i2v_resolution(plan.resolution),
generate_audio: plan.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Veo3Args {
request,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
let resp = enqueue_veo_3_image_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
}
};
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
fn to_i2v_duration(d: FalVeo3Duration) -> Veo3I2vDuration {
match d {
FalVeo3Duration::Default => Veo3I2vDuration::Default,
@@ -66,46 +123,3 @@ fn to_i2v_aspect_ratio(ar: FalVeo3I2vAspectRatio) -> Veo3I2vAspectRatio {
FalVeo3I2vAspectRatio::TallNineSixteen => Veo3I2vAspectRatio::TallNineSixteen,
}
}
pub async fn execute_fal_veo_3(
plan: &PlanFalVeo3,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
FalVeo3Mode::TextToVideo => {
let args = Veo3TextToVideoArgs {
prompt: plan.prompt.as_str(),
negative_prompt: plan.negative_prompt.as_deref(),
api_key: &fal_client.api_key,
duration: to_t2v_duration(plan.duration),
aspect_ratio: plan.t2v_aspect_ratio.map(to_t2v_aspect_ratio).unwrap_or(Veo3T2vAspectRatio::Default),
resolution: to_t2v_resolution(plan.resolution),
generate_audio: plan.generate_audio,
webhook_url: fal_client.webhook_url.as_str(),
};
enqueue_veo_3_text_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
}
FalVeo3Mode::ImageToVideo { image_url } => {
let args = Veo3Args {
image_url: image_url.as_str(),
prompt: plan.prompt.as_str(),
duration: to_i2v_duration(plan.duration),
aspect_ratio: plan.i2v_aspect_ratio.map(to_i2v_aspect_ratio).unwrap_or(Veo3I2vAspectRatio::Auto),
resolution: to_i2v_resolution(plan.resolution),
generate_audio: plan.generate_audio,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
enqueue_veo_3_image_to_video_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
}
};
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -6,21 +9,27 @@ use crate::generate::generate_video::generate_video_response::{
};
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3_fast::PlanFalVeo3Fast;
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{
enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs,
enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastRequest,
};
pub async fn execute_fal_veo_3_fast(
plan: &PlanFalVeo3Fast,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let args = Veo3FastArgs {
prompt: plan.prompt.as_str(),
image_url: plan.start_frame_url.as_str(),
let request = Veo3FastRequest {
prompt: plan.prompt.clone(),
image_url: plan.start_frame_url.clone(),
aspect_ratio: plan.aspect_ratio,
duration: plan.duration,
api_key: &fal_client.api_key,
resolution: plan.resolution,
generate_audio: plan.generate_audio,
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Veo3FastArgs {
request,
api_key: &fal_client.api_key,
webhook_url: fal_client.webhook_url.as_str(),
};
@@ -31,5 +40,6 @@ pub async fn execute_fal_veo_3_fast(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -12,26 +15,27 @@ use fal_client::requests::webhook::video::image::enqueue_veo_3p1_first_last_fram
EnqueueVeo3p1FirstLastFrameImageToVideoArgs,
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio,
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds,
EnqueueVeo3p1FirstLastFrameImageToVideoRequest,
EnqueueVeo3p1FirstLastFrameImageToVideoResolution,
};
use fal_client::requests::webhook::video::image::enqueue_veo_3p1_image_to_video_webhook::{
enqueue_veo_3p1_image_to_video_webhook, EnqueueVeo3p1ImageToVideoArgs,
EnqueueVeo3p1ImageToVideoAspectRatio, EnqueueVeo3p1ImageToVideoDurationSeconds,
EnqueueVeo3p1ImageToVideoResolution,
EnqueueVeo3p1ImageToVideoRequest, EnqueueVeo3p1ImageToVideoResolution,
};
use fal_client::requests::webhook::video::text::enqueue_veo_3p1_text_to_video_webhook::{
enqueue_veo_3p1_text_to_video_webhook, EnqueueVeo3p1TextToVideoArgs,
EnqueueVeo3p1TextToVideoAspectRatio, EnqueueVeo3p1TextToVideoDurationSeconds,
EnqueueVeo3p1TextToVideoResolution,
EnqueueVeo3p1TextToVideoRequest, EnqueueVeo3p1TextToVideoAspectRatio,
EnqueueVeo3p1TextToVideoDurationSeconds, EnqueueVeo3p1TextToVideoResolution,
};
pub async fn execute_fal_veo_3p1(
plan: &PlanFalVeo3p1,
fal_client: &RouterFalClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let webhook_response = match &plan.mode {
let (webhook_response, outbound_request) = match &plan.mode {
FalVeo3p1Mode::TextToVideo => {
let args = EnqueueVeo3p1TextToVideoArgs {
let request = EnqueueVeo3p1TextToVideoRequest {
prompt: plan.prompt.clone(),
duration: plan.duration.map(to_t2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_t2v_aspect_ratio),
@@ -41,26 +45,34 @@ pub async fn execute_fal_veo_3p1(
negative_prompt: plan.negative_prompt.clone(),
seed: None,
auto_fix: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueVeo3p1TextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_veo_3p1_text_to_video_webhook(args).await
(enqueue_veo_3p1_text_to_video_webhook(args).await, outbound)
}
FalVeo3p1Mode::ImageToVideo { start_frame_url } => {
let args = EnqueueVeo3p1ImageToVideoArgs {
let request = EnqueueVeo3p1ImageToVideoRequest {
prompt: plan.prompt.clone(),
image_url: start_frame_url.clone(),
duration: plan.duration.map(to_i2v_duration),
aspect_ratio: plan.aspect_ratio.map(to_i2v_aspect_ratio),
resolution: plan.resolution.map(to_i2v_resolution),
generate_audio: plan.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueVeo3p1ImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_veo_3p1_image_to_video_webhook(args).await
(enqueue_veo_3p1_image_to_video_webhook(args).await, outbound)
}
FalVeo3p1Mode::FirstLastFrame { first_frame_url, last_frame_url } => {
let args = EnqueueVeo3p1FirstLastFrameImageToVideoArgs {
let request = EnqueueVeo3p1FirstLastFrameImageToVideoRequest {
prompt: plan.prompt.clone(),
first_frame_url: first_frame_url.clone(),
last_frame_url: last_frame_url.clone(),
@@ -68,10 +80,14 @@ pub async fn execute_fal_veo_3p1(
aspect_ratio: plan.aspect_ratio.map(to_flf_aspect_ratio),
resolution: plan.resolution.map(to_flf_resolution),
generate_audio: plan.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueVeo3p1FirstLastFrameImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_veo_3p1_first_last_frame_image_to_video_webhook(args).await
(enqueue_veo_3p1_first_last_frame_image_to_video_webhook(args).await, outbound)
}
};
@@ -81,6 +97,7 @@ pub async fn execute_fal_veo_3p1(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
@@ -13,17 +16,18 @@ use fal_client::requests::webhook::video::image::enqueue_veo_3p1_fast_first_last
EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs,
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio,
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds,
EnqueueVeo3p1FastFirstLastFrameImageToVideoRequest,
EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution,
};
use fal_client::requests::webhook::video::image::enqueue_veo_3p1_fast_image_to_video_webhook::{
enqueue_veo_3p1_fast_image_to_video_webhook, EnqueueVeo3p1FastImageToVideoArgs,
EnqueueVeo3p1FastImageToVideoAspectRatio, EnqueueVeo3p1FastImageToVideoDurationSeconds,
EnqueueVeo3p1FastImageToVideoResolution,
EnqueueVeo3p1FastImageToVideoRequest, EnqueueVeo3p1FastImageToVideoResolution,
};
use fal_client::requests::webhook::video::text::enqueue_veo_3p1_fast_text_to_video_webhook::{
enqueue_veo_3p1_fast_text_to_video_webhook, EnqueueVeo3p1FastTextToVideoArgs,
EnqueueVeo3p1FastTextToVideoAspectRatio, EnqueueVeo3p1FastTextToVideoDurationSeconds,
EnqueueVeo3p1FastTextToVideoResolution,
EnqueueVeo3p1FastTextToVideoRequest, EnqueueVeo3p1FastTextToVideoAspectRatio,
EnqueueVeo3p1FastTextToVideoDurationSeconds, EnqueueVeo3p1FastTextToVideoResolution,
};
pub async fn execute_fal_veo_3p1_fast(
@@ -32,9 +36,9 @@ pub async fn execute_fal_veo_3p1_fast(
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let inner = &plan.inner;
let webhook_response = match &inner.mode {
let (webhook_response, outbound_request) = match &inner.mode {
FalVeo3p1Mode::TextToVideo => {
let args = EnqueueVeo3p1FastTextToVideoArgs {
let request = EnqueueVeo3p1FastTextToVideoRequest {
prompt: inner.prompt.clone(),
duration: inner.duration.map(to_t2v_duration),
aspect_ratio: inner.aspect_ratio.map(to_t2v_aspect_ratio),
@@ -44,26 +48,34 @@ pub async fn execute_fal_veo_3p1_fast(
negative_prompt: inner.negative_prompt.clone(),
seed: None,
auto_fix: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueVeo3p1FastTextToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_veo_3p1_fast_text_to_video_webhook(args).await
(enqueue_veo_3p1_fast_text_to_video_webhook(args).await, outbound)
}
FalVeo3p1Mode::ImageToVideo { start_frame_url } => {
let args = EnqueueVeo3p1FastImageToVideoArgs {
let request = EnqueueVeo3p1FastImageToVideoRequest {
prompt: inner.prompt.clone(),
image_url: start_frame_url.clone(),
duration: inner.duration.map(to_i2v_duration),
aspect_ratio: inner.aspect_ratio.map(to_i2v_aspect_ratio),
resolution: inner.resolution.map(to_i2v_resolution),
generate_audio: inner.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueVeo3p1FastImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_veo_3p1_fast_image_to_video_webhook(args).await
(enqueue_veo_3p1_fast_image_to_video_webhook(args).await, outbound)
}
FalVeo3p1Mode::FirstLastFrame { first_frame_url, last_frame_url } => {
let args = EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs {
let request = EnqueueVeo3p1FastFirstLastFrameImageToVideoRequest {
prompt: inner.prompt.clone(),
first_frame_url: first_frame_url.clone(),
last_frame_url: last_frame_url.clone(),
@@ -71,10 +83,14 @@ pub async fn execute_fal_veo_3p1_fast(
aspect_ratio: inner.aspect_ratio.map(to_flf_aspect_ratio),
resolution: inner.resolution.map(to_flf_resolution),
generate_audio: inner.generate_audio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook(args).await
(enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook(args).await, outbound)
}
};
@@ -84,6 +100,7 @@ pub async fn execute_fal_veo_3p1_fast(
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_outbound_request: Some(outbound_request),
}))
}
@@ -1,3 +1,6 @@
use std::fmt::Debug;
use std::sync::Arc;
use muapi_client::api_types::request_id::RequestId as MuapiRequestId;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -24,6 +27,11 @@ pub struct Seedance2proVideoResponsePayload {
pub struct FalVideoResponsePayload {
pub request_id: Option<String>,
pub gateway_request_id: Option<String>,
/// The outbound request that was sent to Fal.
/// Stored as a trait object so any Request type can be captured.
/// Use `format!("{:?}", ...)` or `format!("{:#?}", ...)` to print.
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
#[derive(Clone, Debug)]
@@ -7,6 +7,13 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueFlux2LoraEditImageAngleArgs<'a, R: IntoUrl> {
pub request: EnqueueFlux2LoraEditImageAngleRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueFlux2LoraEditImageAngleRequest {
// Request required
pub image_urls: Vec<String>,
@@ -21,10 +28,6 @@ pub struct EnqueueFlux2LoraEditImageAngleArgs<'a, R: IntoUrl> {
pub lora_scale: Option<f64>,
pub guidance_scale: Option<f64>,
pub num_inference_steps: Option<u32>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -45,7 +48,7 @@ pub enum EnqueueFlux2LoraAngleImageSize {
LandscapeSixteenNine,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueFlux2LoraEditImageAngleArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueFlux2LoraEditImageAngleRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Pricing: $0.021 per megapixel.
// For a 1024x1024 image (~1 MP), that's ~2 cents per image.
@@ -65,7 +68,9 @@ pub async fn enqueue_flux_2_lora_edit_image_angle_webhook<R: IntoUrl>(
args: EnqueueFlux2LoraEditImageAngleArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
let req = args.request;
let num_images = req.num_images
.map(|n| match n {
EnqueueFlux2LoraAngleNumImages::One => 1,
EnqueueFlux2LoraAngleNumImages::Two => 2,
@@ -73,7 +78,7 @@ pub async fn enqueue_flux_2_lora_edit_image_angle_webhook<R: IntoUrl>(
EnqueueFlux2LoraAngleNumImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueFlux2LoraAngleImageSize::Square => "square",
EnqueueFlux2LoraAngleImageSize::SquareHd => "square_hd",
@@ -85,14 +90,14 @@ pub async fn enqueue_flux_2_lora_edit_image_angle_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = Flux2LoraEditImageAngleInput {
image_urls: args.image_urls,
horizontal_angle: args.horizontal_angle,
vertical_angle: args.vertical_angle,
zoom: args.zoom,
lora_scale: args.lora_scale,
image_urls: req.image_urls,
horizontal_angle: req.horizontal_angle,
vertical_angle: req.vertical_angle,
zoom: req.zoom,
lora_scale: req.lora_scale,
image_size,
guidance_scale: args.guidance_scale,
num_inference_steps: args.num_inference_steps,
guidance_scale: req.guidance_scale,
num_inference_steps: req.num_inference_steps,
num_images,
// Constants
enable_safety_checker: Some(false),
@@ -112,7 +117,7 @@ pub async fn enqueue_flux_2_lora_edit_image_angle_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{enqueue_flux_2_lora_edit_image_angle_webhook, EnqueueFlux2LoraEditImageAngleArgs, EnqueueFlux2LoraAngleNumImages, EnqueueFlux2LoraAngleImageSize};
use crate::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{enqueue_flux_2_lora_edit_image_angle_webhook, EnqueueFlux2LoraEditImageAngleArgs, EnqueueFlux2LoraEditImageAngleRequest, EnqueueFlux2LoraAngleNumImages, EnqueueFlux2LoraAngleImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
@@ -124,15 +129,17 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueFlux2LoraEditImageAngleArgs {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(45.0),
vertical_angle: Some(15.0),
zoom: Some(5.0),
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
request: EnqueueFlux2LoraEditImageAngleRequest {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(45.0),
vertical_angle: Some(15.0),
zoom: Some(5.0),
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -141,7 +148,7 @@ mod tests {
Ok(())
}
/// Enqueues 27 angle combinations: 3 horizontal × 3 vertical × 3 zoom levels.
/// Enqueues 27 angle combinations: 3 horizontal x 3 vertical x 3 zoom levels.
#[tokio::test]
#[ignore]
async fn test_angle_grid() -> AnyhowResult<()> {
@@ -156,15 +163,17 @@ mod tests {
for &v in &vertical_angles {
for &z in &zoom_levels {
let args = EnqueueFlux2LoraEditImageAngleArgs {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(h),
vertical_angle: Some(v),
zoom: Some(z),
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
request: EnqueueFlux2LoraEditImageAngleRequest {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(h),
vertical_angle: Some(v),
zoom: Some(z),
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,6 +7,13 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueQwenEdit2511EditImageAngleArgs<'a, R: IntoUrl> {
pub request: EnqueueQwenEdit2511EditImageAngleRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueQwenEdit2511EditImageAngleRequest {
// Request required
pub image_urls: Vec<String>,
@@ -16,16 +23,12 @@ pub struct EnqueueQwenEdit2511EditImageAngleArgs<'a, R: IntoUrl> {
pub zoom: Option<f64>,
// Optional args
pub additional_prompt: Option<&'a str>,
pub additional_prompt: Option<String>,
pub num_images: Option<EnqueueQwenEdit2511AngleNumImages>,
pub image_size: Option<EnqueueQwenEdit2511AngleImageSize>,
pub lora_scale: Option<f64>,
pub guidance_scale: Option<f64>,
pub num_inference_steps: Option<u32>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -46,7 +49,7 @@ pub enum EnqueueQwenEdit2511AngleImageSize {
LandscapeSixteenNine,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueQwenEdit2511EditImageAngleArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueQwenEdit2511EditImageAngleRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Pricing: $0.035 per megapixel.
// For a 1024x1024 image (~1 MP), that's ~4 cents per image.
@@ -66,7 +69,9 @@ pub async fn enqueue_qwen_edit_2511_edit_image_angle_webhook<R: IntoUrl>(
args: EnqueueQwenEdit2511EditImageAngleArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
let req = args.request;
let num_images = req.num_images
.map(|n| match n {
EnqueueQwenEdit2511AngleNumImages::One => 1,
EnqueueQwenEdit2511AngleNumImages::Two => 2,
@@ -74,7 +79,7 @@ pub async fn enqueue_qwen_edit_2511_edit_image_angle_webhook<R: IntoUrl>(
EnqueueQwenEdit2511AngleNumImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueQwenEdit2511AngleImageSize::Square => "square",
EnqueueQwenEdit2511AngleImageSize::SquareHd => "square_hd",
@@ -86,15 +91,15 @@ pub async fn enqueue_qwen_edit_2511_edit_image_angle_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = QwenEdit2511EditImageAngleInput {
image_urls: args.image_urls,
horizontal_angle: args.horizontal_angle,
vertical_angle: args.vertical_angle,
zoom: args.zoom,
additional_prompt: args.additional_prompt.map(|s| s.to_string()),
lora_scale: args.lora_scale,
image_urls: req.image_urls,
horizontal_angle: req.horizontal_angle,
vertical_angle: req.vertical_angle,
zoom: req.zoom,
additional_prompt: req.additional_prompt,
lora_scale: req.lora_scale,
image_size,
guidance_scale: args.guidance_scale,
num_inference_steps: args.num_inference_steps,
guidance_scale: req.guidance_scale,
num_inference_steps: req.num_inference_steps,
num_images,
// Constants
enable_safety_checker: Some(false),
@@ -115,7 +120,7 @@ pub async fn enqueue_qwen_edit_2511_edit_image_angle_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::angle::enqueue_qwen_edit_2511_edit_image_angle_webhook::{enqueue_qwen_edit_2511_edit_image_angle_webhook, EnqueueQwenEdit2511EditImageAngleArgs, EnqueueQwenEdit2511AngleNumImages, EnqueueQwenEdit2511AngleImageSize};
use crate::requests::webhook::image::angle::enqueue_qwen_edit_2511_edit_image_angle_webhook::{enqueue_qwen_edit_2511_edit_image_angle_webhook, EnqueueQwenEdit2511EditImageAngleArgs, EnqueueQwenEdit2511EditImageAngleRequest, EnqueueQwenEdit2511AngleNumImages, EnqueueQwenEdit2511AngleImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
@@ -128,16 +133,18 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueQwenEdit2511EditImageAngleArgs {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(45.0),
vertical_angle: Some(15.0),
zoom: Some(5.0),
additional_prompt: Some("cinematic lighting"),
num_images: Some(EnqueueQwenEdit2511AngleNumImages::One),
image_size: Some(EnqueueQwenEdit2511AngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
request: EnqueueQwenEdit2511EditImageAngleRequest {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(45.0),
vertical_angle: Some(15.0),
zoom: Some(5.0),
additional_prompt: Some("cinematic lighting".to_string()),
num_images: Some(EnqueueQwenEdit2511AngleNumImages::One),
image_size: Some(EnqueueQwenEdit2511AngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -146,7 +153,7 @@ mod tests {
Ok(())
}
/// Enqueues 9 angle combinations: 3 horizontal × 3 vertical × 3 zoom levels.
/// Enqueues 9 angle combinations: 3 horizontal x 3 vertical x 3 zoom levels.
/// Each combination generates one image, for a total of 27 enqueued requests.
#[tokio::test]
#[ignore]
@@ -162,16 +169,18 @@ mod tests {
for &v in &vertical_angles {
for &z in &zoom_levels {
let args = EnqueueQwenEdit2511EditImageAngleArgs {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(h),
vertical_angle: Some(v),
zoom: Some(z),
additional_prompt: None,
num_images: Some(EnqueueQwenEdit2511AngleNumImages::One),
image_size: Some(EnqueueQwenEdit2511AngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
request: EnqueueQwenEdit2511EditImageAngleRequest {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(h),
vertical_angle: Some(v),
zoom: Some(z),
additional_prompt: None,
num_images: Some(EnqueueQwenEdit2511AngleNumImages::One),
image_size: Some(EnqueueQwenEdit2511AngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -5,20 +5,25 @@ use crate::requests::http::image::background::http_rembg_remove_background::{rem
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct RemoveBackgroundRembgWebhookArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub struct RemoveBackgroundRembgWebhookArgs<'a, V: IntoUrl> {
pub request: RemoveBackgroundRembgWebhookRequest,
pub webhook_url: V,
pub api_key: &'a FalApiKey
}
pub async fn remove_background_rembg_webhook<U: IntoUrl, V: IntoUrl>(
args: RemoveBackgroundRembgWebhookArgs<'_, U, V>
#[derive(Clone, Debug)]
pub struct RemoveBackgroundRembgWebhookRequest {
pub image_url: String,
}
pub async fn remove_background_rembg_webhook<V: IntoUrl>(
args: RemoveBackgroundRembgWebhookArgs<'_, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.into_url()?;
let req = args.request;
let request = RembgRemoveBackgroundInput {
image_url: image_url.to_string(),
image_url: req.image_url,
crop_to_bbox: None,
sync_mode: None
};
@@ -35,7 +40,7 @@ pub async fn remove_background_rembg_webhook<U: IntoUrl, V: IntoUrl>(
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::background::remove_background_rembg_webhook::remove_background_rembg_webhook;
use crate::requests::webhook::image::background::remove_background_rembg_webhook::RemoveBackgroundRembgWebhookArgs;
use crate::requests::webhook::image::background::remove_background_rembg_webhook::{RemoveBackgroundRembgWebhookArgs, RemoveBackgroundRembgWebhookRequest};
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
@@ -45,13 +50,15 @@ mod tests {
// XXX: Don't commit secrets!
let api_key = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&api_key);
let args = RemoveBackgroundRembgWebhookArgs {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL,
request: RemoveBackgroundRembgWebhookRequest {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
},
webhook_url: "https://api.storyteller.ai/webhook",
api_key: &api_key,
};
let response = remove_background_rembg_webhook(args).await?;
println!("{:?}", response);
@@ -7,18 +7,18 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub request: EnqueueBytedanceSeedreamV4EditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
// Optional args
#[derive(Clone, Debug)]
pub struct EnqueueBytedanceSeedreamV4EditImageRequest {
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: Option<EnqueueBytedanceSeedreamV4EditImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4EditImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4EditImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -39,23 +39,18 @@ pub enum EnqueueBytedanceSeedreamV4EditImageMaxImages {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4EditImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto,
Auto2k,
Auto4k,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4EditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV4EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.03 per image.
let unit_cost = 3;
@@ -70,29 +65,29 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4EditIma
}
}
pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = args.num_images
.map(|num_images| match num_images {
let num_images = req.num_images
.map(|n| match n {
EnqueueBytedanceSeedreamV4EditImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4EditImageNumImages::Two => 2,
EnqueueBytedanceSeedreamV4EditImageNumImages::Three => 3,
EnqueueBytedanceSeedreamV4EditImageNumImages::Four => 4,
});
let max_images = args.max_images
.map(|num_images| match num_images {
let max_images = req.max_images
.map(|n| match n {
EnqueueBytedanceSeedreamV4EditImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4EditImageMaxImages::Two => 2,
EnqueueBytedanceSeedreamV4EditImageMaxImages::Three => 3,
EnqueueBytedanceSeedreamV4EditImageMaxImages::Four => 4,
});
let image_size = args.image_size
.map(|image_size| match image_size {
let image_size = req.image_size
.map(|s| match s {
EnqueueBytedanceSeedreamV4EditImageSize::Square => "square",
EnqueueBytedanceSeedreamV4EditImageSize::SquareHd => "square_hd",
EnqueueBytedanceSeedreamV4EditImageSize::PortraitFourThree => "portrait_4_3",
@@ -103,32 +98,29 @@ pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV4EditImageSize::Auto2k => "auto_2K",
EnqueueBytedanceSeedreamV4EditImageSize::Auto4k => "auto_4K",
})
.map(|resolution| resolution.to_string());
.map(|s| s.to_string());
let request = SeedreamV4EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Optionals
let input = SeedreamV4EditImageInput {
prompt: req.prompt,
image_urls: req.image_urls,
num_images,
max_images,
image_size,
// Constants
enhance_prompt_mode: Some("standard".to_string()),
enable_safety_checker: Some(false),
};
let result = seedream_4_edit_image(request)
seedream_4_edit_image(input)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
.await
.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs, EnqueueBytedanceSeedreamV4EditImageMaxImages, EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -136,26 +128,22 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4EditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4EditImageSize::Auto2k),
request: EnqueueBytedanceSeedreamV4EditImageRequest {
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
image_urls: vec![GHOST_IMAGE_URL.to_string(), TREX_SKELETON_IMAGE_URL.to_string()],
num_images: Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4EditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4EditImageSize::Auto2k),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v4_edit_image_webhook(args).await?;
let _result = enqueue_bytedance_seedream_v4_edit_image_webhook(args).await?;
Ok(())
}
}
@@ -7,18 +7,18 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4p5EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub request: EnqueueBytedanceSeedreamV4p5EditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
// Optional args
#[derive(Clone, Debug)]
pub struct EnqueueBytedanceSeedreamV4p5EditImageRequest {
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: Option<EnqueueBytedanceSeedreamV4p5EditImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4p5EditImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4p5EditImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -39,22 +39,17 @@ pub enum EnqueueBytedanceSeedreamV4p5EditImageMaxImages {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5EditImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto2k,
Auto4k,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.04 per image.
let unit_cost = 4;
@@ -69,29 +64,29 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5EditI
}
}
pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = args.num_images
.map(|num_images| match num_images {
let num_images = req.num_images
.map(|n| match n {
EnqueueBytedanceSeedreamV4p5EditImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two => 2,
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three => 3,
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four => 4,
});
let max_images = args.max_images
.map(|num_images| match num_images {
let max_images = req.max_images
.map(|n| match n {
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two => 2,
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Three => 3,
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Four => 4,
});
let image_size = args.image_size
.map(|image_size| match image_size {
let image_size = req.image_size
.map(|s| match s {
EnqueueBytedanceSeedreamV4p5EditImageSize::Square => "square",
EnqueueBytedanceSeedreamV4p5EditImageSize::SquareHd => "square_hd",
EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitFourThree => "portrait_4_3",
@@ -101,32 +96,29 @@ pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k => "auto_2K",
EnqueueBytedanceSeedreamV4p5EditImageSize::Auto4k => "auto_4K",
})
.map(|resolution| resolution.to_string());
.map(|s| s.to_string());
let request = SeedreamV4p5EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Optionals
let input = SeedreamV4p5EditImageInput {
prompt: req.prompt,
image_urls: req.image_urls,
num_images,
max_images,
image_size,
// Constants
enable_safety_checker: Some(false),
seed: None,
};
let result = seedream_4p5_edit_image(request)
seedream_4p5_edit_image(input)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
.await
.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageMaxImages, EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -134,27 +126,22 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
//let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k),
request: EnqueueBytedanceSeedreamV4p5EditImageRequest {
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
image_urls: vec![GHOST_IMAGE_URL.to_string(), TREX_SKELETON_IMAGE_URL.to_string()],
num_images: Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args).await?;
let _result = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args).await?;
Ok(())
}
}
@@ -7,18 +7,18 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV5LiteEditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub request: EnqueueBytedanceSeedreamV5LiteEditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
// Optional args
#[derive(Clone, Debug)]
pub struct EnqueueBytedanceSeedreamV5LiteEditImageRequest {
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: Option<EnqueueBytedanceSeedreamV5LiteEditImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV5LiteEditImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV5LiteEditImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -39,22 +39,17 @@ pub enum EnqueueBytedanceSeedreamV5LiteEditImageMaxImages {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV5LiteEditImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto2k,
Auto3k, // NB: v5 uses auto_3K instead of v4's auto_4K
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteEditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteEditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// TODO(bt): Verify actual pricing for Seedream v5 Lite on fal.ai.
let unit_cost = 4;
@@ -69,12 +64,12 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteEdi
}
}
pub async fn enqueue_bytedance_seedream_v5_lite_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV5LiteEditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = args.num_images
let num_images = req.num_images
.map(|n| match n {
EnqueueBytedanceSeedreamV5LiteEditImageNumImages::One => 1,
EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two => 2,
@@ -82,7 +77,7 @@ pub async fn enqueue_bytedance_seedream_v5_lite_edit_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Four => 4,
});
let max_images = args.max_images
let max_images = req.max_images
.map(|n| match n {
EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Two => 2,
@@ -90,7 +85,7 @@ pub async fn enqueue_bytedance_seedream_v5_lite_edit_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueBytedanceSeedreamV5LiteEditImageSize::Square => "square",
EnqueueBytedanceSeedreamV5LiteEditImageSize::SquareHd => "square_hd",
@@ -103,29 +98,26 @@ pub async fn enqueue_bytedance_seedream_v5_lite_edit_image_webhook<R: IntoUrl>(
})
.map(|s| s.to_string());
let request = SeedreamV5LiteEditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Optionals
let input = SeedreamV5LiteEditImageInput {
prompt: req.prompt,
image_urls: req.image_urls,
num_images,
max_images,
image_size,
// Constants
enable_safety_checker: Some(false),
};
let result = http_seedream_5_edit_image(request)
http_seedream_5_edit_image(input)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
.await
.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs, EnqueueBytedanceSeedreamV5LiteEditImageMaxImages, EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -133,26 +125,22 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto2k),
request: EnqueueBytedanceSeedreamV5LiteEditImageRequest {
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
image_urls: vec![GHOST_IMAGE_URL.to_string(), TREX_SKELETON_IMAGE_URL.to_string()],
num_images: Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV5LiteEditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV5LiteEditImageSize::Auto2k),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args).await?;
let _result = enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args).await?;
Ok(())
}
}
@@ -6,15 +6,18 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Flux1DevEditImageArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub image_url: String,
pub num_images: Flux1DevEditImageNumImages,
// Fulfillment
pub request: Flux1DevEditImageRequest,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Flux1DevEditImageRequest {
pub prompt: String,
pub image_url: String,
pub num_images: Flux1DevEditImageNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum Flux1DevEditImageNumImages {
One,
@@ -26,7 +29,9 @@ pub enum Flux1DevEditImageNumImages {
pub async fn enqueue_flux_1_dev_edit_image_webhook<U: IntoUrl>(
args: Flux1DevEditImageArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
Flux1DevEditImageNumImages::One => 1,
Flux1DevEditImageNumImages::Two => 2,
Flux1DevEditImageNumImages::Three => 3,
@@ -34,8 +39,8 @@ pub async fn enqueue_flux_1_dev_edit_image_webhook<U: IntoUrl>(
};
let request = Flux1DevEditImageInput {
prompt: args.prompt.to_string(),
image_url: args.image_url,
prompt: req.prompt,
image_url: req.image_url,
num_images: Some(num_images),
enable_safety_checker: Some(false),
output_format: Some("png".to_string()),
@@ -55,6 +60,7 @@ mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::{
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs, Flux1DevEditImageNumImages,
Flux1DevEditImageRequest,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -67,9 +73,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Flux1DevEditImageArgs {
prompt: "make this image look like a watercolor painting",
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1DevEditImageNumImages::One,
request: Flux1DevEditImageRequest {
prompt: "make this image look like a watercolor painting".to_string(),
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1DevEditImageNumImages::One,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -86,9 +94,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Flux1DevEditImageArgs {
prompt: "turn this into a cyberpunk scene with neon lights",
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1DevEditImageNumImages::Two,
request: Flux1DevEditImageRequest {
prompt: "turn this into a cyberpunk scene with neon lights".to_string(),
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1DevEditImageNumImages::Two,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -6,13 +6,16 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Flux1SchnellEditImageArgs<'a, U: IntoUrl> {
pub request: Flux1SchnellEditImageRequest,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Flux1SchnellEditImageRequest {
pub image_url: String,
pub num_images: Flux1SchnellEditImageNumImages,
pub image_size: Option<Flux1SchnellEditImageSize>,
// Fulfillment
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -36,14 +39,16 @@ pub enum Flux1SchnellEditImageSize {
pub async fn enqueue_flux_1_schnell_edit_image_webhook<U: IntoUrl>(
args: Flux1SchnellEditImageArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
Flux1SchnellEditImageNumImages::One => 1,
Flux1SchnellEditImageNumImages::Two => 2,
Flux1SchnellEditImageNumImages::Three => 3,
Flux1SchnellEditImageNumImages::Four => 4,
};
let image_size = args.image_size.map(|s| match s {
let image_size = req.image_size.map(|s| match s {
Flux1SchnellEditImageSize::Square => "square",
Flux1SchnellEditImageSize::SquareHd => "square_hd",
Flux1SchnellEditImageSize::LandscapeFourByThree => "landscape_4_3",
@@ -53,7 +58,7 @@ pub async fn enqueue_flux_1_schnell_edit_image_webhook<U: IntoUrl>(
}.to_string());
let request = Flux1SchnellEditImageInput {
image_url: args.image_url,
image_url: req.image_url,
num_images: Some(num_images),
image_size,
enable_safety_checker: Some(false),
@@ -74,7 +79,7 @@ mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs,
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageSize,
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageRequest, Flux1SchnellEditImageSize,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -87,9 +92,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Flux1SchnellEditImageArgs {
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1SchnellEditImageNumImages::One,
image_size: None,
request: Flux1SchnellEditImageRequest {
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1SchnellEditImageNumImages::One,
image_size: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -106,9 +113,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Flux1SchnellEditImageArgs {
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1SchnellEditImageNumImages::Two,
image_size: Some(Flux1SchnellEditImageSize::LandscapeSixteenByNine),
request: Flux1SchnellEditImageRequest {
image_url: GHOST_IMAGE_URL.to_string(),
num_images: Flux1SchnellEditImageNumImages::Two,
image_size: Some(Flux1SchnellEditImageSize::LandscapeSixteenByNine),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -5,17 +5,19 @@ use crate::requests::http::image::edit::http_flux_pro_kontext_max_edit::{flux_pr
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxProKontextMaxArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request
pub prompt: &'a str,
pub image_url: U,
pub num_images: FluxProKontextMaxNumImages,
// Fulfillment
pub struct FluxProKontextMaxArgs<'a, R: IntoUrl> {
pub request: FluxProKontextMaxRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct FluxProKontextMaxRequest {
pub prompt: String,
pub image_url: String,
pub num_images: FluxProKontextMaxNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum FluxProKontextMaxNumImages {
One, // Default
@@ -24,11 +26,12 @@ pub enum FluxProKontextMaxNumImages {
Four,
}
pub async fn enqueue_flux_pro_kontext_max_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: FluxProKontextMaxArgs<'_, U, R>
pub async fn enqueue_flux_pro_kontext_max_edit_webhook<R: IntoUrl>(
args: FluxProKontextMaxArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
FluxProKontextMaxNumImages::One => 1,
FluxProKontextMaxNumImages::Two => 2,
FluxProKontextMaxNumImages::Three => 3,
@@ -36,8 +39,8 @@ pub async fn enqueue_flux_pro_kontext_max_edit_webhook<U: IntoUrl, R: IntoUrl>(
};
let request = FluxProKontextMaxEditInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
prompt: req.prompt,
image_url: req.image_url,
num_images: Some(num_images),
// Maybe expose
@@ -61,7 +64,7 @@ pub async fn enqueue_flux_pro_kontext_max_edit_webhook<U: IntoUrl, R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::{enqueue_flux_pro_kontext_max_edit_webhook, FluxProKontextMaxArgs, FluxProKontextMaxNumImages};
use crate::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::{enqueue_flux_pro_kontext_max_edit_webhook, FluxProKontextMaxArgs, FluxProKontextMaxNumImages, FluxProKontextMaxRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TALL_MOCHI_WITH_GLASSES_IMAGE_URL;
@@ -75,11 +78,13 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = FluxProKontextMaxArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
prompt: "turn the glasses into sunglasses, make them sleek sunglasses with black rims, square shaped",
request: FluxProKontextMaxRequest {
prompt: "turn the glasses into sunglasses, make them sleek sunglasses with black rims, square shaped".to_string(),
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(),
num_images: FluxProKontextMaxNumImages::One,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: FluxProKontextMaxNumImages::One,
};
let result = enqueue_flux_pro_kontext_max_edit_webhook(args).await?;
@@ -7,19 +7,21 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Gemini25FlashEditArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: Gemini25FlashEditNumImages,
// Optional
pub aspect_ratio: Option<Gemini25FlashEditAspectRatio>,
// Fulfillment
pub request: Gemini25FlashEditRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Gemini25FlashEditRequest {
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: Gemini25FlashEditNumImages,
// Optional
pub aspect_ratio: Option<Gemini25FlashEditAspectRatio>,
}
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashEditNumImages {
One,
@@ -50,7 +52,7 @@ pub enum Gemini25FlashEditAspectRatio {
}
impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashEditArgs<'_, U> {
impl FalRequestCostCalculator for Gemini25FlashEditRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
match self.num_images {
@@ -66,15 +68,16 @@ impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashEditArgs<'_, U> {
pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
args: Gemini25FlashEditArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
Gemini25FlashEditNumImages::One => 1,
Gemini25FlashEditNumImages::Two => 2,
Gemini25FlashEditNumImages::Three => 3,
Gemini25FlashEditNumImages::Four => 4,
};
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Auto
Gemini25FlashEditAspectRatio::Auto => "auto",
@@ -95,8 +98,8 @@ pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
.map(|aspect_ratio| aspect_ratio.to_string());
let request = Gemini25FlashEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
prompt: req.prompt,
image_urls: req.image_urls,
aspect_ratio,
// Constants
num_images: Some(num_images),
@@ -114,7 +117,7 @@ pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
use crate::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditNumImages, Gemini25FlashEditRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -128,13 +131,15 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Gemini25FlashEditArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
num_images: Gemini25FlashEditNumImages::Two,
aspect_ratio: None,
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
request: Gemini25FlashEditRequest {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
num_images: Gemini25FlashEditNumImages::Two,
aspect_ratio: None,
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -13,12 +13,7 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct GptEditImageByokArgs<'a, V: IntoUrl> {
// Request
pub image_urls: Vec<String>,
pub prompt: &'a str,
pub image_size: GptEditImageSize,
pub num_images: GptEditImageNumImages,
pub quality: GptEditImageQuality,
pub request: GptEditImageByokRequest,
// Fulfillment
pub api_key: &'a FalApiKey,
@@ -26,6 +21,15 @@ pub struct GptEditImageByokArgs<'a, V: IntoUrl> {
pub webhook_url: V,
}
#[derive(Clone, Debug)]
pub struct GptEditImageByokRequest {
pub image_urls: Vec<String>,
pub prompt: String,
pub image_size: GptEditImageSize,
pub num_images: GptEditImageNumImages,
pub quality: GptEditImageQuality,
}
#[derive(Copy, Clone, Debug)]
pub enum GptEditImageSize {
Auto,
@@ -52,7 +56,7 @@ pub enum GptEditImageNumImages{
// NB: These are BYOK, so they're not Fal's prices
impl <U: IntoUrl> FalRequestCostCalculator for GptEditImageByokArgs<'_, U> {
impl FalRequestCostCalculator for GptEditImageByokRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
let base_cost = match self.quality {
GptEditImageQuality::Auto => 17,
@@ -74,22 +78,23 @@ impl <U: IntoUrl> FalRequestCostCalculator for GptEditImageByokArgs<'_, U> {
pub async fn enqueue_gpt_image_1_byok_edit_image_webhook<V: IntoUrl>(
args: GptEditImageByokArgs<'_, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let image_size = match args.image_size {
let image_size = match req.image_size {
GptEditImageSize::Auto => "auto",
GptEditImageSize::Square => "1024x1024",
GptEditImageSize::Horizontal => "1536x1024",
GptEditImageSize::Vertical => "1024x1536",
};
let quality = match args.quality {
let quality = match req.quality {
GptEditImageQuality::Auto => "auto",
GptEditImageQuality::Low => "low",
GptEditImageQuality::Medium => "medium",
GptEditImageQuality::High => "high",
};
let num_images = match args.num_images {
let num_images = match req.num_images {
GptEditImageNumImages::One => 1,
GptEditImageNumImages::Two => 2,
GptEditImageNumImages::Three => 3,
@@ -97,8 +102,8 @@ pub async fn enqueue_gpt_image_1_byok_edit_image_webhook<V: IntoUrl>(
};
let request = GptImage1EditImageInput {
image_urls: args.image_urls,
prompt: args.prompt.to_string(),
image_urls: req.image_urls,
prompt: req.prompt,
image_size: image_size.to_string(),
num_images,
quality: quality.to_string(),
@@ -118,8 +123,8 @@ mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::requests::webhook::image::edit::enqueue_gpt_image_1_byok_edit_image_webhook::{
enqueue_gpt_image_1_byok_edit_image_webhook, GptEditImageByokArgs, GptEditImageNumImages,
GptEditImageQuality, GptEditImageSize,
enqueue_gpt_image_1_byok_edit_image_webhook, GptEditImageByokArgs, GptEditImageByokRequest,
GptEditImageNumImages, GptEditImageQuality, GptEditImageSize,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -138,15 +143,17 @@ mod tests {
let openai_api_key = OpenAiApiKey::from_str(&openai_secret);
let args = GptEditImageByokArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
image_size: GptEditImageSize::Horizontal,
num_images: GptEditImageNumImages::One,
quality: GptEditImageQuality::High,
request: GptEditImageByokRequest {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
image_size: GptEditImageSize::Horizontal,
num_images: GptEditImageNumImages::One,
quality: GptEditImageQuality::High,
},
api_key: &fal_api_key,
openai_api_key: &openai_api_key,
webhook_url: "https://example.com/webhook",
@@ -15,8 +15,15 @@ use reqwest::IntoUrl;
/// (The BYOK variant lives in `enqueue_gpt_image_1_byok_edit_image_webhook.rs`
/// and is deprecated.)
pub struct EnqueueGptImage1EditImageArgs<'a, R: IntoUrl> {
pub request: EnqueueGptImage1EditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueGptImage1EditImageRequest {
// Required
pub prompt: &'a str,
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: EnqueueGptImage1EditImageNumImages,
@@ -27,10 +34,6 @@ pub struct EnqueueGptImage1EditImageArgs<'a, R: IntoUrl> {
pub input_fidelity: Option<EnqueueGptImage1EditImageInputFidelity>,
pub background: Option<EnqueueGptImage1EditImageBackground>,
pub output_format: Option<EnqueueGptImage1EditImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -79,7 +82,7 @@ pub enum EnqueueGptImage1EditImageOutputFormat {
Webp,
}
impl<U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1EditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueGptImage1EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Per fal docs (fal-ai/gpt-image-1/edit-image), pricing matches
// text-to-image (input image tokens are billed separately and not included
@@ -115,14 +118,16 @@ impl<U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1EditImageArgs<'_,
pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
args: EnqueueGptImage1EditImageArgs<'_, R>,
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images: u8 = match args.num_images {
let req = args.request;
let num_images: u8 = match req.num_images {
EnqueueGptImage1EditImageNumImages::One => 1,
EnqueueGptImage1EditImageNumImages::Two => 2,
EnqueueGptImage1EditImageNumImages::Three => 3,
EnqueueGptImage1EditImageNumImages::Four => 4,
};
let image_size = args.image_size.map(|s| {
let image_size = req.image_size.map(|s| {
match s {
EnqueueGptImage1EditImageSize::Auto => "auto",
EnqueueGptImage1EditImageSize::Square => "1024x1024",
@@ -132,7 +137,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
.to_string()
});
let quality = args.quality.map(|q| {
let quality = req.quality.map(|q| {
match q {
EnqueueGptImage1EditImageQuality::Low => "low",
EnqueueGptImage1EditImageQuality::Medium => "medium",
@@ -141,7 +146,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
.to_string()
});
let input_fidelity = args.input_fidelity.map(|f| {
let input_fidelity = req.input_fidelity.map(|f| {
match f {
EnqueueGptImage1EditImageInputFidelity::Low => "low",
EnqueueGptImage1EditImageInputFidelity::High => "high",
@@ -149,7 +154,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
.to_string()
});
let background = args.background.map(|b| {
let background = req.background.map(|b| {
match b {
EnqueueGptImage1EditImageBackground::Auto => "auto",
EnqueueGptImage1EditImageBackground::Transparent => "transparent",
@@ -158,7 +163,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
.to_string()
});
let output_format = args.output_format.map(|f| {
let output_format = req.output_format.map(|f| {
match f {
EnqueueGptImage1EditImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1EditImageOutputFormat::Png => "png",
@@ -168,9 +173,9 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
});
let request = GptImage1NonByokEditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
mask_image_url: args.mask_image_url,
prompt: req.prompt,
image_urls: req.image_urls,
mask_image_url: req.mask_image_url,
num_images: Some(num_images),
image_size,
quality,
@@ -193,7 +198,7 @@ mod tests {
use crate::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{
enqueue_gpt_image_1_edit_image_webhook, EnqueueGptImage1EditImageArgs,
EnqueueGptImage1EditImageNumImages, EnqueueGptImage1EditImageQuality,
EnqueueGptImage1EditImageSize,
EnqueueGptImage1EditImageRequest, EnqueueGptImage1EditImageSize,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -209,19 +214,21 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage1EditImageArgs {
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
num_images: EnqueueGptImage1EditImageNumImages::One,
mask_image_url: None,
image_size: Some(EnqueueGptImage1EditImageSize::Horizontal),
quality: Some(EnqueueGptImage1EditImageQuality::Medium),
input_fidelity: None,
background: None,
output_format: None,
request: EnqueueGptImage1EditImageRequest {
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
num_images: EnqueueGptImage1EditImageNumImages::One,
mask_image_url: None,
image_size: Some(EnqueueGptImage1EditImageSize::Horizontal),
quality: Some(EnqueueGptImage1EditImageQuality::Medium),
input_fidelity: None,
background: None,
output_format: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,22 +7,25 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage1p5EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub request: EnqueueGptImage1p5EditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueGptImage1p5EditImageRequest {
// Required
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: EnqueueGptImage1p5EditImageNumImages,
// Optional args
// Optional
pub mask_image_url: Option<String>,
pub image_size: Option<EnqueueGptImage1p5EditImageSize>,
pub background: Option<EnqueueGptImage1p5EditImageBackground>,
pub quality: Option<EnqueueGptImage1p5EditImageQuality>,
pub input_fidelity: Option<EnqueueGptImage1p5EditImageInputFidelity>,
pub output_format: Option<EnqueueGptImage1p5EditImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -70,8 +73,7 @@ pub enum EnqueueGptImage1p5EditImageOutputFormat {
Webp,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5EditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueGptImage1p5EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost different amounts based on the number of images, quality, and size.
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
@@ -97,19 +99,19 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5EditImageArgs<'
}
}
pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
EnqueueGptImage1p5EditImageNumImages::One => 1,
EnqueueGptImage1p5EditImageNumImages::Two => 2,
EnqueueGptImage1p5EditImageNumImages::Three => 3,
EnqueueGptImage1p5EditImageNumImages::Four => 4,
};
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueGptImage1p5EditImageSize::Square => "1024x1024",
EnqueueGptImage1p5EditImageSize::Wide => "1536x1024",
@@ -117,7 +119,7 @@ pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
})
.map(|resolution| resolution.to_string());
let background = args.background
let background = req.background
.map(|s| match s {
EnqueueGptImage1p5EditImageBackground::Auto => "auto",
EnqueueGptImage1p5EditImageBackground::Transparent => "transparent",
@@ -125,7 +127,7 @@ pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
})
.map(|aspect_ratio| aspect_ratio.to_string());
let quality = args.quality
let quality = req.quality
.map(|s| match s {
EnqueueGptImage1p5EditImageQuality::Low => "low",
EnqueueGptImage1p5EditImageQuality::Medium => "medium",
@@ -133,14 +135,14 @@ pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
})
.map(|aspect_ratio| aspect_ratio.to_string());
let input_fidelity = args.input_fidelity
let input_fidelity = req.input_fidelity
.map(|s| match s {
EnqueueGptImage1p5EditImageInputFidelity::Low => "low",
EnqueueGptImage1p5EditImageInputFidelity::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let output_format = args.output_format
let output_format = req.output_format
.map(|s| match s {
EnqueueGptImage1p5EditImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1p5EditImageOutputFormat::Png => "png",
@@ -150,12 +152,12 @@ pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
.unwrap_or_else(|| "png".to_string());
let request = GptImage1p5EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
prompt: req.prompt,
image_urls: req.image_urls,
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
mask_image_url: args.mask_image_url,
mask_image_url: req.mask_image_url,
image_size,
background,
quality,
@@ -173,7 +175,10 @@ pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs, EnqueueGptImage1p5EditImageNumImages};
use crate::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs,
EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageRequest,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -187,21 +192,23 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage1p5EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: EnqueueGptImage1p5EditImageNumImages::Two,
mask_image_url: None,
image_size: None,
background: None,
quality: None,
input_fidelity: None,
request: EnqueueGptImage1p5EditImageRequest {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
num_images: EnqueueGptImage1p5EditImageNumImages::Two,
mask_image_url: None,
image_size: None,
background: None,
quality: None,
input_fidelity: None,
output_format: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_1p5_image_edit_webhook(args).await?;
@@ -9,20 +9,23 @@ use crate::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhoo
use crate::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::EnqueueGptImage2EditImageSize::SquareHd;
pub struct EnqueueGptImage2EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub request: EnqueueGptImage2EditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueGptImage2EditImageRequest {
// Required
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: EnqueueGptImage2EditImageNumImages,
// Optional args
// Optional
pub mask_url: Option<String>,
pub image_size: Option<EnqueueGptImage2EditImageSize>,
pub quality: Option<EnqueueGptImage2EditImageQuality>,
pub output_format: Option<EnqueueGptImage2EditImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -58,8 +61,7 @@ pub enum EnqueueGptImage2EditImageOutputFormat {
Webp,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage2EditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueGptImage2EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Cost table (per image) by approximate pixel dimensions:
//
@@ -99,19 +101,19 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage2EditImageArgs<'_,
}
}
pub async fn enqueue_gpt_image_2_edit_image_webhook<R: IntoUrl>(
args: EnqueueGptImage2EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
EnqueueGptImage2EditImageNumImages::One => 1,
EnqueueGptImage2EditImageNumImages::Two => 2,
EnqueueGptImage2EditImageNumImages::Three => 3,
EnqueueGptImage2EditImageNumImages::Four => 4,
};
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueGptImage2EditImageSize::SquareHd => "square_hd",
EnqueueGptImage2EditImageSize::Square => "square",
@@ -123,7 +125,7 @@ pub async fn enqueue_gpt_image_2_edit_image_webhook<R: IntoUrl>(
})
.map(|size| size.to_string());
let quality = args.quality
let quality = req.quality
.map(|s| match s {
EnqueueGptImage2EditImageQuality::Low => "low",
EnqueueGptImage2EditImageQuality::Medium => "medium",
@@ -131,7 +133,7 @@ pub async fn enqueue_gpt_image_2_edit_image_webhook<R: IntoUrl>(
})
.map(|quality| quality.to_string());
let output_format = args.output_format
let output_format = req.output_format
.map(|s| match s {
EnqueueGptImage2EditImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage2EditImageOutputFormat::Png => "png",
@@ -141,12 +143,12 @@ pub async fn enqueue_gpt_image_2_edit_image_webhook<R: IntoUrl>(
.unwrap_or_else(|| "png".to_string());
let request = GptImage2EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
prompt: req.prompt,
image_urls: req.image_urls,
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
mask_url: args.mask_url,
mask_url: req.mask_url,
image_size,
quality,
};
@@ -162,7 +164,10 @@ pub async fn enqueue_gpt_image_2_edit_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{enqueue_gpt_image_2_edit_image_webhook, EnqueueGptImage2EditImageArgs, EnqueueGptImage2EditImageNumImages};
use crate::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{
enqueue_gpt_image_2_edit_image_webhook, EnqueueGptImage2EditImageArgs,
EnqueueGptImage2EditImageNumImages, EnqueueGptImage2EditImageRequest,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -176,19 +181,21 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage2EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: EnqueueGptImage2EditImageNumImages::Two,
mask_url: None,
image_size: None,
quality: None,
request: EnqueueGptImage2EditImageRequest {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
num_images: EnqueueGptImage2EditImageNumImages::Two,
mask_url: None,
image_size: None,
quality: None,
output_format: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_2_edit_image_webhook(args).await?;
@@ -7,18 +7,21 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueNanoBanana2EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub request: EnqueueNanoBanana2EditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueNanoBanana2EditImageRequest {
// Required
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: EnqueueNanoBanana2EditImageNumImages,
// Optional args
// Optional
pub resolution: Option<EnqueueNanoBanana2EditImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBanana2EditImageAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -58,8 +61,7 @@ pub enum EnqueueNanoBanana2EditImageAspectRatio {
NineBySixteen,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBanana2EditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueNanoBanana2EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// TODO(bt): Verify actual pricing for Nano Banana 2 on fal.ai.
// 4K outputs may be charged at double the standard rate.
@@ -80,19 +82,19 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBanana2EditImageArgs<'
}
}
pub async fn enqueue_nano_banana_2_edit_image_webhook<R: IntoUrl>(
args: EnqueueNanoBanana2EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
EnqueueNanoBanana2EditImageNumImages::One => 1,
EnqueueNanoBanana2EditImageNumImages::Two => 2,
EnqueueNanoBanana2EditImageNumImages::Three => 3,
EnqueueNanoBanana2EditImageNumImages::Four => 4,
};
let resolution = args.resolution
let resolution = req.resolution
.map(|resolution| match resolution {
EnqueueNanoBanana2EditImageResolution::HalfK => "0.5K",
EnqueueNanoBanana2EditImageResolution::OneK => "1K",
@@ -101,7 +103,7 @@ pub async fn enqueue_nano_banana_2_edit_image_webhook<R: IntoUrl>(
})
.map(|r| r.to_string());
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|ar| match ar {
// Auto
EnqueueNanoBanana2EditImageAspectRatio::Auto => "auto",
@@ -122,8 +124,8 @@ pub async fn enqueue_nano_banana_2_edit_image_webhook<R: IntoUrl>(
.map(|ar| ar.to_string());
let request = NanoBanana2EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
prompt: req.prompt,
image_urls: req.image_urls,
num_images: Some(num_images),
// Optionals
aspect_ratio,
@@ -147,7 +149,11 @@ pub async fn enqueue_nano_banana_2_edit_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs, EnqueueNanoBanana2EditImageAspectRatio, EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageResolution};
use crate::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{
enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs,
EnqueueNanoBanana2EditImageAspectRatio, EnqueueNanoBanana2EditImageNumImages,
EnqueueNanoBanana2EditImageRequest, EnqueueNanoBanana2EditImageResolution,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL, WHITE_HOUSE_SUNSET_IMAGE_URL};
@@ -161,15 +167,17 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueNanoBanana2EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: EnqueueNanoBanana2EditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBanana2EditImageResolution::TwoK),
request: EnqueueNanoBanana2EditImageRequest {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
num_images: EnqueueNanoBanana2EditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBanana2EditImageResolution::TwoK),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -188,15 +196,17 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueNanoBanana2EditImageArgs {
image_urls: vec![
WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "Put the scared man and the t-rex in front of the white house scene. Make the man afraid of the t-rex.",
num_images: EnqueueNanoBanana2EditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBanana2EditImageResolution::TwoK),
request: EnqueueNanoBanana2EditImageRequest {
image_urls: vec![
WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "Put the scared man and the t-rex in front of the white house scene. Make the man afraid of the t-rex.".to_string(),
num_images: EnqueueNanoBanana2EditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBanana2EditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBanana2EditImageResolution::TwoK),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,18 +7,20 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueNanoBananaProEditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub request: EnqueueNanoBananaProEditImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueNanoBananaProEditImageRequest {
pub prompt: String,
pub image_urls: Vec<String>,
pub num_images: EnqueueNanoBananaProEditImageNumImages,
// Optional args
pub resolution: Option<EnqueueNanoBananaProEditImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBananaProEditImageAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -58,7 +60,7 @@ pub enum EnqueueNanoBananaProEditImageAspectRatio {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProEditImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueNanoBananaProEditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.15 per image.
// For $1.00, you can run this model 7 times.
@@ -84,15 +86,16 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProEditImageArgs
pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
args: EnqueueNanoBananaProEditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
EnqueueNanoBananaProEditImageNumImages::One => 1,
EnqueueNanoBananaProEditImageNumImages::Two => 2,
EnqueueNanoBananaProEditImageNumImages::Three => 3,
EnqueueNanoBananaProEditImageNumImages::Four => 4,
};
let resolution = args.resolution
let resolution = req.resolution
.map(|resolution| match resolution {
EnqueueNanoBananaProEditImageResolution::OneK => "1K",
EnqueueNanoBananaProEditImageResolution::TwoK => "2K",
@@ -100,7 +103,7 @@ pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Auto
EnqueueNanoBananaProEditImageAspectRatio::Auto => "auto",
@@ -121,8 +124,8 @@ pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
.map(|aspect_ratio| aspect_ratio.to_string());
let request = NanoBananaProEditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
prompt: req.prompt,
image_urls: req.image_urls,
num_images: Some(num_images),
// Optionals
aspect_ratio,
@@ -142,7 +145,7 @@ pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
use crate::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageRequest, EnqueueNanoBananaProEditImageResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -156,15 +159,17 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueNanoBananaProEditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: EnqueueNanoBananaProEditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBananaProEditImageResolution::TwoK),
request: EnqueueNanoBananaProEditImageRequest {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
],
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
num_images: EnqueueNanoBananaProEditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBananaProEditImageResolution::TwoK),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -6,11 +6,17 @@ use crate::requests::http::image::edit::http_qwen_image_edit::{qwen_image_edit,
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct QwenImageEditArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_url: U,
pub struct QwenImageEditArgs<'a, R: IntoUrl> {
pub request: QwenImageEditRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct QwenImageEditRequest {
pub prompt: String,
pub image_url: String,
// Request optional
pub num_images: Option<QwenImageEditNumImages>,
pub image_size: Option<QwenImageEditSize>,
@@ -21,10 +27,6 @@ pub struct QwenImageEditArgs<'a, U: IntoUrl, R: IntoUrl> {
/// Options: 'none', 'regular'. Higher acceleration increases speed.
/// 'regular' balances speed and quality. Default value: "none"
pub acceleration: Option<String>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -47,7 +49,7 @@ pub enum QwenImageEditSize {
}
impl <U: IntoUrl, R: IntoUrl> FalRequestCostCalculator for QwenImageEditArgs<'_, U, R> {
impl FalRequestCostCalculator for QwenImageEditRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.03 per megapixel. -- Ugh, not calculating that
let base_cost = 3;
@@ -63,11 +65,12 @@ impl <U: IntoUrl, R: IntoUrl> FalRequestCostCalculator for QwenImageEditArgs<'_,
}
pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: QwenImageEditArgs<'_, U, R>
pub async fn enqueue_qwen_image_edit_webhook<R: IntoUrl>(
args: QwenImageEditArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = args.num_images
let num_images = req.num_images
.map(|num| match num {
QwenImageEditNumImages::One => 1,
QwenImageEditNumImages::Two => 2,
@@ -75,7 +78,7 @@ pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
QwenImageEditNumImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|size| match size {
QwenImageEditSize::Square => "square",
QwenImageEditSize::SquareHd => "square_hd",
@@ -87,8 +90,8 @@ pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
.map(|size| size.to_string());
let request = QwenImageEditInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
prompt: req.prompt,
image_url: req.image_url,
image_size,
num_images,
@@ -116,7 +119,7 @@ pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_qwen_image_edit_webhook::{enqueue_qwen_image_edit_webhook, QwenImageEditArgs, QwenImageEditNumImages};
use crate::requests::webhook::image::edit::enqueue_qwen_image_edit_webhook::{enqueue_qwen_image_edit_webhook, QwenImageEditArgs, QwenImageEditNumImages, QwenImageEditRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
@@ -130,12 +133,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = QwenImageEditArgs {
image_url: MOUNTAIN_TREE_IMAGE_URL,
prompt: "put christmas lights on the tree, add snow to the mountains",
num_images: Some(QwenImageEditNumImages::One),
image_size: None,
negative_prompt: None,
acceleration: None,
request: QwenImageEditRequest {
prompt: "put christmas lights on the tree, add snow to the mountains".to_string(),
image_url: MOUNTAIN_TREE_IMAGE_URL.to_string(),
num_images: Some(QwenImageEditNumImages::One),
image_size: None,
negative_prompt: None,
acceleration: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -5,23 +5,26 @@ use crate::requests::http::image::edit::http_seededit_v3_edit::{seededit_v3_edit
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct SeedEditV3EditArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_url: U,
// Fulfillment
pub struct SeedEditV3EditArgs<'a, R: IntoUrl> {
pub request: SeedEditV3EditRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
pub async fn enqueue_seededit_v3_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: SeedEditV3EditArgs<'_, U, R>
#[derive(Clone, Debug)]
pub struct SeedEditV3EditRequest {
pub prompt: String,
pub image_url: String,
}
pub async fn enqueue_seededit_v3_edit_webhook<R: IntoUrl>(
args: SeedEditV3EditArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let request = SeedEditV3EditInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
prompt: req.prompt,
image_url: req.image_url,
// Constants
guidance_scale: None,
@@ -39,7 +42,7 @@ pub async fn enqueue_seededit_v3_edit_webhook<U: IntoUrl, R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_seededit_v3_edit_webhook::{enqueue_seededit_v3_edit_webhook, SeedEditV3EditArgs};
use crate::requests::webhook::image::edit::enqueue_seededit_v3_edit_webhook::{enqueue_seededit_v3_edit_webhook, SeedEditV3EditArgs, SeedEditV3EditRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
@@ -53,8 +56,10 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = SeedEditV3EditArgs {
image_url: MOUNTAIN_TREE_IMAGE_URL,
prompt: "put christmas lights on the tree, add snow to the mountains",
request: SeedEditV3EditRequest {
prompt: "put christmas lights on the tree, add snow to the mountains".to_string(),
image_url: MOUNTAIN_TREE_IMAGE_URL.to_string(),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -5,18 +5,20 @@ use crate::requests::http::image::infill::http_flux_dev_juggernaut_infill::{flux
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxDevJuggernautInfillArgs<'a, U: IntoUrl, R: IntoUrl, L: IntoUrl> {
// Request
pub prompt: &'a str,
pub image_url: U,
pub mask_url: R,
pub num_images: FluxDevJuggernautInfillNumImages,
// Fulfillment
pub webhook_url: L,
pub struct FluxDevJuggernautInfillArgs<'a, R: IntoUrl> {
pub request: FluxDevJuggernautInfillRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct FluxDevJuggernautInfillRequest {
pub prompt: String,
pub image_url: String,
pub mask_url: String,
pub num_images: FluxDevJuggernautInfillNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum FluxDevJuggernautInfillNumImages {
One, // Default
@@ -25,11 +27,13 @@ pub enum FluxDevJuggernautInfillNumImages {
Four,
}
pub async fn enqueue_flux_dev_juggernaut_infill_webhook<U: IntoUrl, R: IntoUrl, L: IntoUrl>(
args: FluxDevJuggernautInfillArgs<'_, U, R, L>
pub async fn enqueue_flux_dev_juggernaut_infill_webhook<R: IntoUrl>(
args: FluxDevJuggernautInfillArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
FluxDevJuggernautInfillNumImages::One => 1,
FluxDevJuggernautInfillNumImages::Two => 2,
FluxDevJuggernautInfillNumImages::Three => 3,
@@ -37,9 +41,9 @@ pub async fn enqueue_flux_dev_juggernaut_infill_webhook<U: IntoUrl, R: IntoUrl,
};
let request = FluxDevJuggernautInfillInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
mask_url: args.mask_url.as_str().to_string(),
prompt: req.prompt,
image_url: req.image_url,
mask_url: req.mask_url,
num_images: Some(num_images),
// Maybe expose
@@ -66,7 +70,7 @@ pub async fn enqueue_flux_dev_juggernaut_infill_webhook<U: IntoUrl, R: IntoUrl,
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::infill::enqueue_flux_dev_juggernaut_infill_webhook::{enqueue_flux_dev_juggernaut_infill_webhook, FluxDevJuggernautInfillArgs, FluxDevJuggernautInfillNumImages};
use crate::requests::webhook::image::infill::enqueue_flux_dev_juggernaut_infill_webhook::{enqueue_flux_dev_juggernaut_infill_webhook, FluxDevJuggernautInfillArgs, FluxDevJuggernautInfillRequest, FluxDevJuggernautInfillNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
@@ -80,12 +84,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = FluxDevJuggernautInfillArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
mask_url: TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL,
prompt: "slick sunglasses, cool glasses, reflection in glasses lenses",
request: FluxDevJuggernautInfillRequest {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(),
mask_url: TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL.to_string(),
prompt: "slick sunglasses, cool glasses, reflection in glasses lenses".to_string(),
num_images: FluxDevJuggernautInfillNumImages::Two,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: FluxDevJuggernautInfillNumImages::Two,
};
let result = enqueue_flux_dev_juggernaut_infill_webhook(args).await?;
@@ -5,18 +5,20 @@ use crate::requests::http::image::infill::http_flux_pro_1_infill::{flux_pro_1_in
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxPro1InfillArgs<'a, U: IntoUrl, R: IntoUrl, L: IntoUrl> {
// Request
pub prompt: &'a str,
pub image_url: U,
pub mask_url: R,
pub num_images: FluxPro1InfillNumImages,
// Fulfillment
pub webhook_url: L,
pub struct FluxPro1InfillArgs<'a, R: IntoUrl> {
pub request: FluxPro1InfillRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct FluxPro1InfillRequest {
pub prompt: String,
pub image_url: String,
pub mask_url: String,
pub num_images: FluxPro1InfillNumImages,
}
#[derive(Copy, Clone, Debug)]
pub enum FluxPro1InfillNumImages {
One, // Default
@@ -25,11 +27,13 @@ pub enum FluxPro1InfillNumImages {
Four,
}
pub async fn enqueue_flux_pro_1_infill_webhook<U: IntoUrl, R: IntoUrl, L: IntoUrl>(
args: FluxPro1InfillArgs<'_, U, R, L>
pub async fn enqueue_flux_pro_1_infill_webhook<R: IntoUrl>(
args: FluxPro1InfillArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
FluxPro1InfillNumImages::One => 1,
FluxPro1InfillNumImages::Two => 2,
FluxPro1InfillNumImages::Three => 3,
@@ -37,9 +41,9 @@ pub async fn enqueue_flux_pro_1_infill_webhook<U: IntoUrl, R: IntoUrl, L: IntoUr
};
let request = FluxPro1InfillInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
mask_url: args.mask_url.as_str().to_string(),
prompt: req.prompt,
image_url: req.image_url,
mask_url: req.mask_url,
num_images: Some(num_images),
// Maybe expose
@@ -62,7 +66,7 @@ pub async fn enqueue_flux_pro_1_infill_webhook<U: IntoUrl, R: IntoUrl, L: IntoUr
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::infill::enqueue_flux_pro_1_infill_webhook::{enqueue_flux_pro_1_infill_webhook, FluxPro1InfillArgs, FluxPro1InfillNumImages};
use crate::requests::webhook::image::infill::enqueue_flux_pro_1_infill_webhook::{enqueue_flux_pro_1_infill_webhook, FluxPro1InfillArgs, FluxPro1InfillRequest, FluxPro1InfillNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
@@ -76,12 +80,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = FluxPro1InfillArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
mask_url: TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL,
prompt: "slick sunglasses, cool glasses, reflection in glasses lenses",
request: FluxPro1InfillRequest {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(),
mask_url: TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL.to_string(),
prompt: "slick sunglasses, cool glasses, reflection in glasses lenses".to_string(),
num_images: FluxPro1InfillNumImages::One,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: FluxPro1InfillNumImages::One,
};
let result = enqueue_flux_pro_1_infill_webhook(args).await?;
@@ -7,17 +7,20 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4TextToImageArgs<'a, R: IntoUrl> {
pub request: EnqueueBytedanceSeedreamV4TextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueBytedanceSeedreamV4TextToImageRequest {
// Request required
pub prompt: &'a str,
pub prompt: String,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV4TextToImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4TextToImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4TextToImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -54,7 +57,7 @@ pub enum EnqueueBytedanceSeedreamV4TextToImageSize {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4TextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV4TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Copied from edit image version
// Your request will cost $0.03 per image.
@@ -75,7 +78,9 @@ pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
let req = args.request;
let num_images = req.num_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4TextToImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4TextToImageNumImages::Two => 2,
@@ -83,7 +88,7 @@ pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV4TextToImageNumImages::Four => 4,
});
let max_images = args.max_images
let max_images = req.max_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4TextToImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two => 2,
@@ -91,7 +96,7 @@ pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|image_size| match image_size {
EnqueueBytedanceSeedreamV4TextToImageSize::Square => "square",
EnqueueBytedanceSeedreamV4TextToImageSize::SquareHd => "square_hd",
@@ -106,7 +111,7 @@ pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
.map(|resolution| resolution.to_string());
let request = SeedreamV4TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
// Optionals
num_images,
max_images,
@@ -127,7 +132,7 @@ pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, EnqueueBytedanceSeedreamV4TextToImageMaxImages, EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize};
use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, EnqueueBytedanceSeedreamV4TextToImageMaxImages, EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageRequest, EnqueueBytedanceSeedreamV4TextToImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -140,10 +145,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
prompt: "an anime girl is riding a t-rex in the forest",
num_images: Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine),
request: EnqueueBytedanceSeedreamV4TextToImageRequest {
prompt: "an anime girl is riding a t-rex in the forest".to_string(),
num_images: Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -8,17 +8,20 @@ use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostC
use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageNumImages};
pub struct EnqueueBytedanceSeedreamV4p5TextToImageArgs<'a, R: IntoUrl> {
pub request: EnqueueBytedanceSeedreamV4p5TextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueBytedanceSeedreamV4p5TextToImageRequest {
// Request required
pub prompt: &'a str,
pub prompt: String,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV4p5TextToImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4p5TextToImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4p5TextToImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -54,7 +57,7 @@ pub enum EnqueueBytedanceSeedreamV4p5TextToImageSize {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// NB: Copied from edit image costs
// Your request will cost $0.04 per image.
@@ -75,7 +78,9 @@ pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
let req = args.request;
let num_images = req.num_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two => 2,
@@ -83,7 +88,7 @@ pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four => 4,
});
let max_images = args.max_images
let max_images = req.max_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two => 2,
@@ -91,7 +96,7 @@ pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|image_size| match image_size {
EnqueueBytedanceSeedreamV4p5TextToImageSize::Square => "square",
EnqueueBytedanceSeedreamV4p5TextToImageSize::SquareHd => "square_hd",
@@ -105,7 +110,7 @@ pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
.map(|resolution| resolution.to_string());
let request = SeedreamV4p5TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
// Optionals
num_images,
max_images,
@@ -126,7 +131,7 @@ pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, EnqueueBytedanceSeedreamV4p5TextToImageMaxImages, EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize};
use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, EnqueueBytedanceSeedreamV4p5TextToImageMaxImages, EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageRequest, EnqueueBytedanceSeedreamV4p5TextToImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -139,10 +144,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
prompt: "an anime girl is riding a t-rex in the forest",
num_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine),
request: EnqueueBytedanceSeedreamV4p5TextToImageRequest {
prompt: "an anime girl is riding a t-rex in the forest".to_string(),
num_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,17 +7,20 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'a, R: IntoUrl> {
pub request: EnqueueBytedanceSeedreamV5LiteTextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueBytedanceSeedreamV5LiteTextToImageRequest {
// Request required
pub prompt: &'a str,
pub prompt: String,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV5LiteTextToImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV5LiteTextToImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -53,7 +56,7 @@ pub enum EnqueueBytedanceSeedreamV5LiteTextToImageSize {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueBytedanceSeedreamV5LiteTextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// TODO(bt): Verify actual pricing for Seedream v5 Lite on fal.ai.
let unit_cost = 4;
@@ -73,7 +76,9 @@ pub async fn enqueue_bytedance_seedream_v5_lite_text_to_image_webhook<R: IntoUrl
args: EnqueueBytedanceSeedreamV5LiteTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
let req = args.request;
let num_images = req.num_images
.map(|n| match n {
EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One => 1,
EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two => 2,
@@ -81,7 +86,7 @@ pub async fn enqueue_bytedance_seedream_v5_lite_text_to_image_webhook<R: IntoUrl
EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Four => 4,
});
let max_images = args.max_images
let max_images = req.max_images
.map(|n| match n {
EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Two => 2,
@@ -89,7 +94,7 @@ pub async fn enqueue_bytedance_seedream_v5_lite_text_to_image_webhook<R: IntoUrl
EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Four => 4,
});
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueBytedanceSeedreamV5LiteTextToImageSize::Square => "square",
EnqueueBytedanceSeedreamV5LiteTextToImageSize::SquareHd => "square_hd",
@@ -103,7 +108,7 @@ pub async fn enqueue_bytedance_seedream_v5_lite_text_to_image_webhook<R: IntoUrl
.map(|s| s.to_string());
let request = SeedreamV5LiteTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
// Optionals
num_images,
max_images,
@@ -123,7 +128,7 @@ pub async fn enqueue_bytedance_seedream_v5_lite_text_to_image_webhook<R: IntoUrl
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs, EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages, EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageSize};
use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs, EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages, EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageRequest, EnqueueBytedanceSeedreamV5LiteTextToImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -136,10 +141,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
prompt: "an anime girl is riding a t-rex in the forest",
num_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeSixteenNine),
request: EnqueueBytedanceSeedreamV5LiteTextToImageRequest {
prompt: "an anime girl is riding a t-rex in the forest".to_string(),
num_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::LandscapeSixteenNine),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -6,9 +6,14 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Flux1DevArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub request: Flux1DevRequest,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Flux1DevRequest {
pub prompt: String,
pub aspect_ratio: Flux1DevAspectRatio,
pub num_images: Flux1DevNumImages,
}
@@ -36,14 +41,16 @@ pub async fn enqueue_flux_1_dev_text_to_image_webhook<U: IntoUrl>(
args: Flux1DevArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
Flux1DevNumImages::One => 1,
Flux1DevNumImages::Two => 2,
Flux1DevNumImages::Three => 3,
Flux1DevNumImages::Four => 4,
};
let image_size = match args.aspect_ratio {
let image_size = match req.aspect_ratio {
Flux1DevAspectRatio::Square => "square",
Flux1DevAspectRatio::SquareHd => "square_hd",
Flux1DevAspectRatio::LandscapeFourByThree => "landscape_4_3",
@@ -53,7 +60,7 @@ pub async fn enqueue_flux_1_dev_text_to_image_webhook<U: IntoUrl>(
};
let request = Flux1DevTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
image_size: Some(image_size.to_string()),
// Maybe abstract
@@ -77,7 +84,7 @@ pub async fn enqueue_flux_1_dev_text_to_image_webhook<U: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages};
use crate::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs, Flux1DevAspectRatio, Flux1DevNumImages, Flux1DevRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -90,11 +97,13 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Flux1DevArgs {
prompt: "a giant robot fighting a dragon in a futuristic city",
request: Flux1DevRequest {
prompt: "a giant robot fighting a dragon in a futuristic city".to_string(),
num_images: Flux1DevNumImages::One,
aspect_ratio: Flux1DevAspectRatio::LandscapeFourByThree,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: Flux1DevNumImages::One,
aspect_ratio: Flux1DevAspectRatio::LandscapeFourByThree
};
let result = enqueue_flux_1_dev_text_to_image_webhook(args).await?;
@@ -6,9 +6,14 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Flux1SchnellArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub request: Flux1SchnellRequest,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Flux1SchnellRequest {
pub prompt: String,
pub aspect_ratio: Flux1SchnellAspectRatio,
pub num_images: Flux1SchnellNumImages,
}
@@ -36,14 +41,16 @@ pub async fn enqueue_flux_1_schnell_text_to_image_webhook<U: IntoUrl>(
args: Flux1SchnellArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
Flux1SchnellNumImages::One => 1,
Flux1SchnellNumImages::Two => 2,
Flux1SchnellNumImages::Three => 3,
Flux1SchnellNumImages::Four => 4,
};
let image_size = match args.aspect_ratio {
let image_size = match req.aspect_ratio {
Flux1SchnellAspectRatio::Square => "square",
Flux1SchnellAspectRatio::SquareHd => "square_hd",
Flux1SchnellAspectRatio::LandscapeFourByThree => "landscape_4_3",
@@ -53,7 +60,7 @@ pub async fn enqueue_flux_1_schnell_text_to_image_webhook<U: IntoUrl>(
};
let request = Flux1SchnellTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
image_size: Some(image_size.to_string()),
// Maybe abstract
@@ -76,7 +83,7 @@ pub async fn enqueue_flux_1_schnell_text_to_image_webhook<U: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages};
use crate::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs, Flux1SchnellAspectRatio, Flux1SchnellNumImages, Flux1SchnellRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -89,11 +96,13 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Flux1SchnellArgs {
prompt: "a giant robot fighting a dragon in a futuristic city",
request: Flux1SchnellRequest {
prompt: "a giant robot fighting a dragon in a futuristic city".to_string(),
num_images: Flux1SchnellNumImages::One,
aspect_ratio: Flux1SchnellAspectRatio::LandscapeSixteenByNine,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: Flux1SchnellNumImages::One,
aspect_ratio: Flux1SchnellAspectRatio::LandscapeSixteenByNine,
};
let result = enqueue_flux_1_schnell_text_to_image_webhook(args).await?;
@@ -7,9 +7,14 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxPro11Args<'a, U: IntoUrl> {
pub prompt: &'a str,
pub request: FluxPro11Request,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct FluxPro11Request {
pub prompt: String,
pub aspect_ratio: FluxPro11AspectRatio,
pub num_images: FluxPro11NumImages,
}
@@ -35,7 +40,7 @@ pub enum FluxPro11NumImages {
}
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11Args<'_, U> {
impl FalRequestCostCalculator for FluxPro11Request {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.04 per megapixel, billed by rounding up to the nearest
// megapixel. Default image_size values are ~1MP, so 4 cents per image.
@@ -55,14 +60,16 @@ pub async fn enqueue_flux_pro_11_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11Args<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
FluxPro11NumImages::One => 1,
FluxPro11NumImages::Two => 2,
FluxPro11NumImages::Three => 3,
FluxPro11NumImages::Four => 4,
};
let image_size = match args.aspect_ratio {
let image_size = match req.aspect_ratio {
FluxPro11AspectRatio::Square => "square",
FluxPro11AspectRatio::SquareHd => "square_hd",
FluxPro11AspectRatio::LandscapeFourByThree => "landscape_4_3",
@@ -72,7 +79,7 @@ pub async fn enqueue_flux_pro_11_text_to_image_webhook<U: IntoUrl>(
};
let request = FluxPro11TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
image_size: Some(image_size.to_string()),
// Maybe expose
@@ -96,7 +103,7 @@ pub async fn enqueue_flux_pro_11_text_to_image_webhook<U: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args, FluxPro11AspectRatio, FluxPro11NumImages};
use crate::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args, FluxPro11AspectRatio, FluxPro11NumImages, FluxPro11Request};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -109,11 +116,13 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = FluxPro11Args {
prompt: "a giant red panda fighting a dragon in a futuristic city",
request: FluxPro11Request {
prompt: "a giant red panda fighting a dragon in a futuristic city".to_string(),
num_images: FluxPro11NumImages::One,
aspect_ratio: FluxPro11AspectRatio::LandscapeSixteenByNine,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
num_images: FluxPro11NumImages::One,
aspect_ratio: FluxPro11AspectRatio::LandscapeSixteenByNine,
};
let result = enqueue_flux_pro_11_text_to_image_webhook(args).await?;
@@ -7,9 +7,14 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct FluxPro11UltraArgs<'a, U: IntoUrl> {
pub prompt: &'a str,
pub request: FluxPro11UltraRequest,
pub webhook_url: U,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct FluxPro11UltraRequest {
pub prompt: String,
pub aspect_ratio: FluxPro11UltraAspectRatio,
pub num_images: FluxPro11UltraNumImages,
}
@@ -37,7 +42,7 @@ pub enum FluxPro11UltraNumImages {
}
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11UltraArgs<'_, U> {
impl FalRequestCostCalculator for FluxPro11UltraRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.06 per image.
let base_cost = 6;
@@ -56,14 +61,16 @@ pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11UltraArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
FluxPro11UltraNumImages::One => 1,
FluxPro11UltraNumImages::Two => 2,
FluxPro11UltraNumImages::Three => 3,
FluxPro11UltraNumImages::Four => 4,
};
let aspect_ratio = match args.aspect_ratio {
let aspect_ratio = match req.aspect_ratio {
FluxPro11UltraAspectRatio::Square => "1:1",
FluxPro11UltraAspectRatio::LandscapeThreeByTwo => "3:2",
FluxPro11UltraAspectRatio::LandscapeFourByThree => "4:3",
@@ -76,7 +83,7 @@ pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
};
let request = FluxPro11UltraTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
aspect_ratio: Some(aspect_ratio.to_string()),
// Maybe expose
@@ -97,3 +104,34 @@ pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs, FluxPro11UltraAspectRatio, FluxPro11UltraNumImages, FluxPro11UltraRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = FluxPro11UltraArgs {
request: FluxPro11UltraRequest {
prompt: "a giant robot fighting a dragon in a futuristic city".to_string(),
num_images: FluxPro11UltraNumImages::One,
aspect_ratio: FluxPro11UltraAspectRatio::LandscapeSixteenByNine,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_flux_pro_11_ultra_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -8,18 +8,21 @@ use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostC
use crate::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
pub struct Gemini25FlashTextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: Gemini25FlashTextToImageNumImages,
// Optional
pub aspect_ratio: Option<Gemini25FlashTextToImageAspectRatio>,
// Fulfillment
pub request: Gemini25FlashTextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Gemini25FlashTextToImageRequest {
// Required
pub prompt: String,
pub num_images: Gemini25FlashTextToImageNumImages,
// Optional
pub aspect_ratio: Option<Gemini25FlashTextToImageAspectRatio>,
}
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashTextToImageNumImages {
One,
@@ -48,7 +51,7 @@ pub enum Gemini25FlashTextToImageAspectRatio {
}
impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashTextToImageArgs<'_, U> {
impl FalRequestCostCalculator for Gemini25FlashTextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// NB: This was copied from the image-to-image case.
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
@@ -66,14 +69,16 @@ pub async fn enqueue_gemini_25_flash_text_to_image_webhook<R: IntoUrl>(
args: Gemini25FlashTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
Gemini25FlashTextToImageNumImages::One => 1,
Gemini25FlashTextToImageNumImages::Two => 2,
Gemini25FlashTextToImageNumImages::Three => 3,
Gemini25FlashTextToImageNumImages::Four => 4,
};
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Square
Gemini25FlashTextToImageAspectRatio::OneByOne => "1:1",
@@ -92,7 +97,7 @@ pub async fn enqueue_gemini_25_flash_text_to_image_webhook<R: IntoUrl>(
.map(|aspect_ratio| aspect_ratio.to_string());
let request = Gemini25FlashTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
aspect_ratio,
// Constants
num_images: Some(num_images),
@@ -110,7 +115,7 @@ pub async fn enqueue_gemini_25_flash_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs, Gemini25FlashTextToImageNumImages};
use crate::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs, Gemini25FlashTextToImageNumImages, Gemini25FlashTextToImageRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -123,9 +128,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Gemini25FlashTextToImageArgs {
prompt: "a warrior on the field of battle, final fantasy style, lots of hungry raptors rushing the heroes",
num_images: Gemini25FlashTextToImageNumImages::Two,
aspect_ratio: None,
request: Gemini25FlashTextToImageRequest {
prompt: "a warrior on the field of battle, final fantasy style, lots of hungry raptors rushing the heroes".to_string(),
num_images: Gemini25FlashTextToImageNumImages::Two,
aspect_ratio: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -13,11 +13,7 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct GptTextToImageByokArgs<'a, V: IntoUrl> {
// Request
pub prompt: &'a str,
pub image_size: GptTextToImageSize,
pub num_images: GptTextToImageNumImages,
pub quality: GptTextToImageQuality,
pub request: GptTextToImageByokRequest,
// Fulfillment
pub api_key: &'a FalApiKey,
@@ -25,6 +21,14 @@ pub struct GptTextToImageByokArgs<'a, V: IntoUrl> {
pub webhook_url: V,
}
#[derive(Clone, Debug)]
pub struct GptTextToImageByokRequest {
pub prompt: String,
pub image_size: GptTextToImageSize,
pub num_images: GptTextToImageNumImages,
pub quality: GptTextToImageQuality,
}
#[derive(Copy, Clone, Debug)]
pub enum GptTextToImageSize{
Auto,
@@ -51,7 +55,7 @@ pub enum GptTextToImageNumImages{
// NB: These are BYOK, so they're not Fal's prices
impl <U: IntoUrl> FalRequestCostCalculator for GptTextToImageByokArgs<'_, U> {
impl FalRequestCostCalculator for GptTextToImageByokRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Can't find details, so using this: https://www.reddit.com/r/OpenAI/comments/1krfwa1/pricing_gpt_image_1_model/
// Prompts are billed similarly to other GPT models. Image outputs cost approximately $0.01 (low), $0.04 (medium),
@@ -78,22 +82,24 @@ pub async fn enqueue_gpt_image_1_byok_text_to_image_webhook<V: IntoUrl>(
args: GptTextToImageByokArgs<'_, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
// auto, 1024x1024, 1536x1024, 1024x1536
let image_size = match args.image_size {
let image_size = match req.image_size {
GptTextToImageSize::Auto => "auto",
GptTextToImageSize::Square => "1024x1024",
GptTextToImageSize::Horizontal => "1536x1024",
GptTextToImageSize::Vertical => "1024x1536",
};
let quality = match args.quality {
let quality = match req.quality {
GptTextToImageQuality::Auto => "auto",
GptTextToImageQuality::Low => "low",
GptTextToImageQuality::Medium => "medium",
GptTextToImageQuality::High => "high",
};
let num_images = match args.num_images {
let num_images = match req.num_images {
GptTextToImageNumImages::One => 1,
GptTextToImageNumImages::Two => 2,
GptTextToImageNumImages::Three => 3,
@@ -101,7 +107,7 @@ pub async fn enqueue_gpt_image_1_byok_text_to_image_webhook<V: IntoUrl>(
};
let request = GptImage1TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
image_size: image_size.to_string(),
num_images,
quality: quality.to_string(),
@@ -122,7 +128,8 @@ mod tests {
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::requests::webhook::image::text::enqueue_gpt_image_1_byok_text_to_image_webhook::{
enqueue_gpt_image_1_byok_text_to_image_webhook, GptTextToImageByokArgs,
GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize,
GptTextToImageByokRequest, GptTextToImageNumImages, GptTextToImageQuality,
GptTextToImageSize,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -138,10 +145,12 @@ mod tests {
let openai_api_key = OpenAiApiKey::from_str(&openai_secret);
let args = GptTextToImageByokArgs {
prompt: "an anime girl riding on the back of a t-rex",
image_size: GptTextToImageSize::Horizontal,
num_images: GptTextToImageNumImages::One,
quality: GptTextToImageQuality::High,
request: GptTextToImageByokRequest {
prompt: "an anime girl riding on the back of a t-rex".to_string(),
image_size: GptTextToImageSize::Horizontal,
num_images: GptTextToImageNumImages::One,
quality: GptTextToImageQuality::High,
},
api_key: &fal_api_key,
openai_api_key: &openai_api_key,
webhook_url: "https://example.com/webhook",
@@ -15,8 +15,15 @@ use reqwest::IntoUrl;
/// (The BYOK variant lives in `enqueue_gpt_image_1_byok_text_to_image_webhook.rs`
/// and is deprecated.)
pub struct EnqueueGptImage1TextToImageArgs<'a, R: IntoUrl> {
pub request: EnqueueGptImage1TextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueGptImage1TextToImageRequest {
// Required
pub prompt: &'a str,
pub prompt: String,
pub num_images: EnqueueGptImage1TextToImageNumImages,
// Optional
@@ -24,10 +31,6 @@ pub struct EnqueueGptImage1TextToImageArgs<'a, R: IntoUrl> {
pub quality: Option<EnqueueGptImage1TextToImageQuality>,
pub background: Option<EnqueueGptImage1TextToImageBackground>,
pub output_format: Option<EnqueueGptImage1TextToImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -70,7 +73,7 @@ pub enum EnqueueGptImage1TextToImageOutputFormat {
Webp,
}
impl<U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1TextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueGptImage1TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Per fal docs (fal-ai/gpt-image-1/text-to-image):
// Low: $0.011 (1024x1024) / $0.016 (other) per image
@@ -105,14 +108,16 @@ impl<U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1TextToImageArgs<'_
pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
args: EnqueueGptImage1TextToImageArgs<'_, R>,
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images: u8 = match args.num_images {
let req = args.request;
let num_images: u8 = match req.num_images {
EnqueueGptImage1TextToImageNumImages::One => 1,
EnqueueGptImage1TextToImageNumImages::Two => 2,
EnqueueGptImage1TextToImageNumImages::Three => 3,
EnqueueGptImage1TextToImageNumImages::Four => 4,
};
let image_size = args.image_size.map(|s| {
let image_size = req.image_size.map(|s| {
match s {
EnqueueGptImage1TextToImageSize::Auto => "auto",
EnqueueGptImage1TextToImageSize::Square => "1024x1024",
@@ -122,7 +127,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
.to_string()
});
let quality = args.quality.map(|q| {
let quality = req.quality.map(|q| {
match q {
EnqueueGptImage1TextToImageQuality::Low => "low",
EnqueueGptImage1TextToImageQuality::Medium => "medium",
@@ -131,7 +136,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
.to_string()
});
let background = args.background.map(|b| {
let background = req.background.map(|b| {
match b {
EnqueueGptImage1TextToImageBackground::Auto => "auto",
EnqueueGptImage1TextToImageBackground::Transparent => "transparent",
@@ -140,7 +145,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
.to_string()
});
let output_format = args.output_format.map(|f| {
let output_format = req.output_format.map(|f| {
match f {
EnqueueGptImage1TextToImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1TextToImageOutputFormat::Png => "png",
@@ -150,7 +155,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
});
let request = GptImage1NonByokTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
image_size,
quality,
@@ -172,7 +177,7 @@ mod tests {
use crate::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
enqueue_gpt_image_1_text_to_image_webhook, EnqueueGptImage1TextToImageArgs,
EnqueueGptImage1TextToImageNumImages, EnqueueGptImage1TextToImageQuality,
EnqueueGptImage1TextToImageSize,
EnqueueGptImage1TextToImageRequest, EnqueueGptImage1TextToImageSize,
};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -185,12 +190,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage1TextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueGptImage1TextToImageNumImages::One,
image_size: Some(EnqueueGptImage1TextToImageSize::Horizontal),
quality: Some(EnqueueGptImage1TextToImageQuality::Medium),
background: None,
output_format: None,
request: EnqueueGptImage1TextToImageRequest {
prompt: "an anime girl riding on the back of a t-rex".to_string(),
num_images: EnqueueGptImage1TextToImageNumImages::One,
image_size: Some(EnqueueGptImage1TextToImageSize::Horizontal),
quality: Some(EnqueueGptImage1TextToImageQuality::Medium),
background: None,
output_format: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,19 +7,22 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage1p5TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub request: EnqueueGptImage1p5TextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueGptImage1p5TextToImageRequest {
// Required
pub prompt: String,
pub num_images: EnqueueGptImage1p5TextToImageNumImages,
// Optional args
// Optional
pub image_size: Option<EnqueueGptImage1p5TextToImageSize>,
pub background: Option<EnqueueGptImage1p5TextToImageBackground>,
pub quality: Option<EnqueueGptImage1p5TextToImageQuality>,
pub output_format: Option<EnqueueGptImage1p5TextToImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -62,7 +65,7 @@ pub enum EnqueueGptImage1p5TextToImageOutputFormat {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5TextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueGptImage1p5TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// NB: copied from edit image case - should be correct, but not verified.
// Your request will cost different amounts based on the number of images, quality, and size.
@@ -94,15 +97,16 @@ pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
let num_images = match args.num_images {
let num_images = match req.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => 1,
EnqueueGptImage1p5TextToImageNumImages::Two => 2,
EnqueueGptImage1p5TextToImageNumImages::Three => 3,
EnqueueGptImage1p5TextToImageNumImages::Four => 4,
};
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueGptImage1p5TextToImageSize::Square => "1024x1024",
EnqueueGptImage1p5TextToImageSize::Wide => "1536x1024",
@@ -110,7 +114,7 @@ pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
})
.map(|resolution| resolution.to_string());
let background = args.background
let background = req.background
.map(|s| match s {
EnqueueGptImage1p5TextToImageBackground::Auto => "auto",
EnqueueGptImage1p5TextToImageBackground::Transparent => "transparent",
@@ -118,7 +122,7 @@ pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
})
.map(|aspect_ratio| aspect_ratio.to_string());
let quality = args.quality
let quality = req.quality
.map(|s| match s {
EnqueueGptImage1p5TextToImageQuality::Low => "low",
EnqueueGptImage1p5TextToImageQuality::Medium => "medium",
@@ -126,7 +130,7 @@ pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
})
.map(|aspect_ratio| aspect_ratio.to_string());
let output_format = args.output_format
let output_format = req.output_format
.map(|s| match s {
EnqueueGptImage1p5TextToImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1p5TextToImageOutputFormat::Png => "png",
@@ -135,15 +139,8 @@ pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
.map(|aspect_ratio| aspect_ratio.to_string())
.unwrap_or_else(|| "png".to_string());
let num_images = match args.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => 1,
EnqueueGptImage1p5TextToImageNumImages::Two => 2,
EnqueueGptImage1p5TextToImageNumImages::Three => 3,
EnqueueGptImage1p5TextToImageNumImages::Four => 4,
};
let request = GptImage1p5TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
@@ -163,7 +160,7 @@ pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs, EnqueueGptImage1p5TextToImageNumImages};
use crate::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs, EnqueueGptImage1p5TextToImageNumImages, EnqueueGptImage1p5TextToImageRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -176,14 +173,16 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage1p5TextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueGptImage1p5TextToImageNumImages::Two,
image_size: None,
background: None,
quality: None,
request: EnqueueGptImage1p5TextToImageRequest {
prompt: "an anime girl riding on the back of a t-rex".to_string(),
num_images: EnqueueGptImage1p5TextToImageNumImages::Two,
image_size: None,
background: None,
quality: None,
output_format: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_1p5_text_to_image_webhook(args).await?;
@@ -7,18 +7,21 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage2TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub request: EnqueueGptImage2TextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueGptImage2TextToImageRequest {
// Required
pub prompt: String,
pub num_images: EnqueueGptImage2TextToImageNumImages,
// Optional args
// Optional
pub image_size: Option<EnqueueGptImage2TextToImageSize>,
pub quality: Option<EnqueueGptImage2TextToImageQuality>,
pub output_format: Option<EnqueueGptImage2TextToImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -54,7 +57,7 @@ pub enum EnqueueGptImage2TextToImageOutputFormat {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage2TextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueGptImage2TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Cost table (per image) by approximate pixel dimensions:
//
@@ -97,14 +100,16 @@ pub async fn enqueue_gpt_image_2_text_to_image_webhook<R: IntoUrl>(
args: EnqueueGptImage2TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
EnqueueGptImage2TextToImageNumImages::One => 1,
EnqueueGptImage2TextToImageNumImages::Two => 2,
EnqueueGptImage2TextToImageNumImages::Three => 3,
EnqueueGptImage2TextToImageNumImages::Four => 4,
};
let image_size = args.image_size
let image_size = req.image_size
.map(|s| match s {
EnqueueGptImage2TextToImageSize::SquareHd => "square_hd",
EnqueueGptImage2TextToImageSize::Square => "square",
@@ -115,7 +120,7 @@ pub async fn enqueue_gpt_image_2_text_to_image_webhook<R: IntoUrl>(
})
.map(|size| size.to_string());
let quality = args.quality
let quality = req.quality
.map(|s| match s {
EnqueueGptImage2TextToImageQuality::Low => "low",
EnqueueGptImage2TextToImageQuality::Medium => "medium",
@@ -123,7 +128,7 @@ pub async fn enqueue_gpt_image_2_text_to_image_webhook<R: IntoUrl>(
})
.map(|quality| quality.to_string());
let output_format = args.output_format
let output_format = req.output_format
.map(|s| match s {
EnqueueGptImage2TextToImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage2TextToImageOutputFormat::Png => "png",
@@ -133,7 +138,7 @@ pub async fn enqueue_gpt_image_2_text_to_image_webhook<R: IntoUrl>(
.unwrap_or_else(|| "png".to_string());
let request = GptImage2TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
@@ -152,7 +157,7 @@ pub async fn enqueue_gpt_image_2_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_gpt_image_2_text_to_image_webhook::{enqueue_gpt_image_2_text_to_image_webhook, EnqueueGptImage2TextToImageArgs, EnqueueGptImage2TextToImageNumImages};
use crate::requests::webhook::image::text::enqueue_gpt_image_2_text_to_image_webhook::{enqueue_gpt_image_2_text_to_image_webhook, EnqueueGptImage2TextToImageArgs, EnqueueGptImage2TextToImageNumImages, EnqueueGptImage2TextToImageRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -165,13 +170,15 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage2TextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueGptImage2TextToImageNumImages::Two,
image_size: None,
quality: None,
request: EnqueueGptImage2TextToImageRequest {
prompt: "an anime girl riding on the back of a t-rex".to_string(),
num_images: EnqueueGptImage2TextToImageNumImages::Two,
image_size: None,
quality: None,
output_format: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_2_text_to_image_webhook(args).await?;
@@ -7,19 +7,22 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueNanoBanana2TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: EnqueueNanoBanana2TextToImageNumImages,
// Optional args
pub resolution: Option<EnqueueNanoBanana2TextToImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBanana2TextToImageAspectRatio>,
// Fulfillment
pub request: EnqueueNanoBanana2TextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueNanoBanana2TextToImageRequest {
// Required
pub prompt: String,
pub num_images: EnqueueNanoBanana2TextToImageNumImages,
// Optional
pub resolution: Option<EnqueueNanoBanana2TextToImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBanana2TextToImageAspectRatio>,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBanana2TextToImageNumImages {
One,
@@ -58,7 +61,7 @@ pub enum EnqueueNanoBanana2TextToImageAspectRatio {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBanana2TextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueNanoBanana2TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// TODO(bt): Verify actual pricing for Nano Banana 2 on fal.ai.
// 4K outputs may be charged at double the standard rate.
@@ -84,14 +87,16 @@ pub async fn enqueue_nano_banana_2_text_to_image_webhook<R: IntoUrl>(
args: EnqueueNanoBanana2TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
EnqueueNanoBanana2TextToImageNumImages::One => 1,
EnqueueNanoBanana2TextToImageNumImages::Two => 2,
EnqueueNanoBanana2TextToImageNumImages::Three => 3,
EnqueueNanoBanana2TextToImageNumImages::Four => 4,
};
let resolution = args.resolution
let resolution = req.resolution
.map(|resolution| match resolution {
EnqueueNanoBanana2TextToImageResolution::HalfK => "0.5K",
EnqueueNanoBanana2TextToImageResolution::OneK => "1K",
@@ -100,7 +105,7 @@ pub async fn enqueue_nano_banana_2_text_to_image_webhook<R: IntoUrl>(
})
.map(|r| r.to_string());
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|ar| match ar {
// Auto
EnqueueNanoBanana2TextToImageAspectRatio::Auto => "auto",
@@ -121,7 +126,7 @@ pub async fn enqueue_nano_banana_2_text_to_image_webhook<R: IntoUrl>(
.map(|ar| ar.to_string());
let request = NanoBanana2TextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
// Optionals
aspect_ratio,
@@ -145,7 +150,7 @@ pub async fn enqueue_nano_banana_2_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs, EnqueueNanoBanana2TextToImageAspectRatio, EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageResolution};
use crate::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs, EnqueueNanoBanana2TextToImageAspectRatio, EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageRequest, EnqueueNanoBanana2TextToImageResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -158,10 +163,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueNanoBanana2TextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueNanoBanana2TextToImageNumImages::One,
aspect_ratio: Some(EnqueueNanoBanana2TextToImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBanana2TextToImageResolution::TwoK),
request: EnqueueNanoBanana2TextToImageRequest {
prompt: "an anime girl riding on the back of a t-rex".to_string(),
num_images: EnqueueNanoBanana2TextToImageNumImages::One,
aspect_ratio: Some(EnqueueNanoBanana2TextToImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBanana2TextToImageResolution::TwoK),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -8,19 +8,22 @@ use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostC
use crate::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
pub struct EnqueueNanoBananaProTextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: EnqueueNanoBananaProTextToImageNumImages,
// Optional args
pub resolution: Option<EnqueueNanoBananaProTextToImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBananaProTextToImageAspectRatio>,
// Fulfillment
pub request: EnqueueNanoBananaProTextToImageRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueNanoBananaProTextToImageRequest {
// Required
pub prompt: String,
pub num_images: EnqueueNanoBananaProTextToImageNumImages,
// Optional
pub resolution: Option<EnqueueNanoBananaProTextToImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBananaProTextToImageAspectRatio>,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProTextToImageNumImages {
One,
@@ -56,7 +59,7 @@ pub enum EnqueueNanoBananaProTextToImageAspectRatio {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProTextToImageArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueNanoBananaProTextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.15 per image.
// For $1.00, you can run this model 7 times.
@@ -84,14 +87,16 @@ pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
args: EnqueueNanoBananaProTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
let req = args.request;
let num_images = match req.num_images {
EnqueueNanoBananaProTextToImageNumImages::One => 1,
EnqueueNanoBananaProTextToImageNumImages::Two => 2,
EnqueueNanoBananaProTextToImageNumImages::Three => 3,
EnqueueNanoBananaProTextToImageNumImages::Four => 4,
};
let resolution = args.resolution
let resolution = req.resolution
.map(|resolution| match resolution {
EnqueueNanoBananaProTextToImageResolution::OneK => "1K",
EnqueueNanoBananaProTextToImageResolution::TwoK => "2K",
@@ -99,7 +104,7 @@ pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Square
EnqueueNanoBananaProTextToImageAspectRatio::OneByOne => "1:1",
@@ -118,7 +123,7 @@ pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
.map(|aspect_ratio| aspect_ratio.to_string());
let request = NanoBananaProTextToImageInput {
prompt: args.prompt.to_string(),
prompt: req.prompt,
num_images: Some(num_images),
// Optionals
aspect_ratio,
@@ -138,7 +143,7 @@ pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs, EnqueueNanoBananaProTextToImageAspectRatio, EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution};
use crate::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs, EnqueueNanoBananaProTextToImageAspectRatio, EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageRequest, EnqueueNanoBananaProTextToImageResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -151,10 +156,12 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueNanoBananaProTextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueNanoBananaProTextToImageNumImages::One,
aspect_ratio: Some(EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBananaProTextToImageResolution::TwoK),
request: EnqueueNanoBananaProTextToImageRequest {
prompt: "an anime girl riding on the back of a t-rex".to_string(),
num_images: EnqueueNanoBananaProTextToImageNumImages::One,
aspect_ratio: Some(EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBananaProTextToImageResolution::TwoK),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -8,21 +8,24 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueHunyuan3dV3ImageTo3dArgs<'a, R: IntoUrl> {
// Request required
pub request: EnqueueHunyuan3dV3ImageTo3dRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueHunyuan3dV3ImageTo3dRequest {
// Required
pub image_url: String,
// Optional args
pub back_image_url : Option<String>,
pub left_image_url : Option<String>,
pub right_image_url : Option<String>,
// Optional
pub back_image_url: Option<String>,
pub left_image_url: Option<String>,
pub right_image_url: Option<String>,
pub face_count: Option<u32>,
pub generate_type: Option<EnqueueHunyuan3dV3ImageTo3dGenerateType>,
pub polygon_type: Option<EnqueueHunyuan3dV3ImageTo3dPolygonType>,
pub enable_pbr: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -38,7 +41,7 @@ pub enum EnqueueHunyuan3dV3ImageTo3dPolygonType {
Quadrilateral,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueHunyuan3dV3ImageTo3dArgs<'_, R> {
impl FalRequestCostCalculator for EnqueueHunyuan3dV3ImageTo3dRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "Your request will cost $0.375 per generation.
// For $1.00, you can run this model 2 times.
@@ -77,7 +80,9 @@ pub async fn enqueue_hunyuan3d_v3_image_to_3d_webhook<R: IntoUrl>(
args: EnqueueHunyuan3dV3ImageTo3dArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let generate_type= args.generate_type
let req = args.request;
let generate_type = req.generate_type
.map(|t| match t {
EnqueueHunyuan3dV3ImageTo3dGenerateType::Normal => "Normal",
EnqueueHunyuan3dV3ImageTo3dGenerateType::LowPoly => "LowPoly",
@@ -85,7 +90,7 @@ pub async fn enqueue_hunyuan3d_v3_image_to_3d_webhook<R: IntoUrl>(
})
.map(|s| s.to_string());
let polygon_type = args.polygon_type
let polygon_type = req.polygon_type
.map(|t| match t {
EnqueueHunyuan3dV3ImageTo3dPolygonType::Triangle => "triangle",
EnqueueHunyuan3dV3ImageTo3dPolygonType::Quadrilateral => "quadrilateral",
@@ -93,15 +98,15 @@ pub async fn enqueue_hunyuan3d_v3_image_to_3d_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = Hunyuan3dV3ImageTo3dInput {
input_image_url: args.image_url,
input_image_url: req.image_url,
// Optionals
back_image_url: args.back_image_url,
left_image_url: args.left_image_url,
right_image_url: args.right_image_url,
face_count: args.face_count,
back_image_url: req.back_image_url,
left_image_url: req.left_image_url,
right_image_url: req.right_image_url,
face_count: req.face_count,
generate_type,
polygon_type,
enable_pbr: args.enable_pbr,
enable_pbr: req.enable_pbr,
};
let result = hunyuan3d_v3_image_to_3d(request)
@@ -115,7 +120,7 @@ pub async fn enqueue_hunyuan3d_v3_image_to_3d_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::object::enqueue_hunyuan3d_v3_image_to_3d_webhook::{enqueue_hunyuan3d_v3_image_to_3d_webhook, EnqueueHunyuan3dV3ImageTo3dArgs};
use crate::requests::webhook::object::enqueue_hunyuan3d_v3_image_to_3d_webhook::{enqueue_hunyuan3d_v3_image_to_3d_webhook, EnqueueHunyuan3dV3ImageTo3dArgs, EnqueueHunyuan3dV3ImageTo3dRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
@@ -129,16 +134,18 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHunyuan3dV3ImageTo3dArgs {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
back_image_url: None,
left_image_url: None,
right_image_url: None,
face_count: None,
generate_type: None,
polygon_type: None,
request: EnqueueHunyuan3dV3ImageTo3dRequest {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
back_image_url: None,
left_image_url: None,
right_image_url: None,
face_count: None,
generate_type: None,
polygon_type: None,
enable_pbr: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
enable_pbr: None,
};
let result = enqueue_hunyuan3d_v3_image_to_3d_webhook(args).await?;
@@ -8,19 +8,22 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueHunyuan3dV3SketchTo3dArgs<'a, R: IntoUrl> {
// Request required
pub request: EnqueueHunyuan3dV3SketchTo3dRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueHunyuan3dV3SketchTo3dRequest {
// Required
pub prompt: String,
pub image_url: String,
// Optional args
// Optional
pub face_count: Option<u32>,
pub generate_type: Option<EnqueueHunyuan3dV3SketchTo3dGenerateType>,
pub polygon_type: Option<EnqueueHunyuan3dV3SketchTo3dPolygonType>,
pub enable_pbr: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -36,7 +39,7 @@ pub enum EnqueueHunyuan3dV3SketchTo3dPolygonType {
Quadrilateral,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueHunyuan3dV3SketchTo3dArgs<'_, R> {
impl FalRequestCostCalculator for EnqueueHunyuan3dV3SketchTo3dRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Generation types: Normal costs $0.375,
// LowPoly costs $0.45,
@@ -66,7 +69,9 @@ pub async fn enqueue_hunyuan3d_v3_sketch_to_3d_webhook<R: IntoUrl>(
args: EnqueueHunyuan3dV3SketchTo3dArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let generate_type = args.generate_type
let req = args.request;
let generate_type = req.generate_type
.map(|t| match t {
EnqueueHunyuan3dV3SketchTo3dGenerateType::Normal => "Normal",
EnqueueHunyuan3dV3SketchTo3dGenerateType::LowPoly => "LowPoly",
@@ -74,7 +79,7 @@ pub async fn enqueue_hunyuan3d_v3_sketch_to_3d_webhook<R: IntoUrl>(
})
.map(|s| s.to_string());
let polygon_type = args.polygon_type
let polygon_type = req.polygon_type
.map(|t| match t {
EnqueueHunyuan3dV3SketchTo3dPolygonType::Triangle => "triangle",
EnqueueHunyuan3dV3SketchTo3dPolygonType::Quadrilateral => "quadrilateral",
@@ -82,13 +87,13 @@ pub async fn enqueue_hunyuan3d_v3_sketch_to_3d_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = Hunyuan3dV3SketchTo3dInput {
prompt: args.prompt,
input_image_url: args.image_url,
prompt: req.prompt,
input_image_url: req.image_url,
// Optionals
face_count: args.face_count,
face_count: req.face_count,
generate_type,
polygon_type,
enable_pbr: args.enable_pbr,
enable_pbr: req.enable_pbr,
};
let result = hunyuan3d_v3_sketch_to_3d(request)
@@ -102,7 +107,7 @@ pub async fn enqueue_hunyuan3d_v3_sketch_to_3d_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::object::enqueue_hunyuan3d_v3_sketch_to_3d_webhook::{enqueue_hunyuan3d_v3_sketch_to_3d_webhook, EnqueueHunyuan3dV3SketchTo3dArgs};
use crate::requests::webhook::object::enqueue_hunyuan3d_v3_sketch_to_3d_webhook::{enqueue_hunyuan3d_v3_sketch_to_3d_webhook, EnqueueHunyuan3dV3SketchTo3dArgs, EnqueueHunyuan3dV3SketchTo3dRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
@@ -116,14 +121,16 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHunyuan3dV3SketchTo3dArgs {
prompt: "A cute robot".to_string(),
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
face_count: None,
generate_type: None,
polygon_type: None,
request: EnqueueHunyuan3dV3SketchTo3dRequest {
prompt: "A cute robot".to_string(),
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
face_count: None,
generate_type: None,
polygon_type: None,
enable_pbr: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
enable_pbr: None,
};
let result = enqueue_hunyuan3d_v3_sketch_to_3d_webhook(args).await?;
@@ -8,18 +8,21 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueHunyuan3dV3TextTo3dArgs<'a, R: IntoUrl> {
// Request required
pub request: EnqueueHunyuan3dV3TextTo3dRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueHunyuan3dV3TextTo3dRequest {
// Required
pub prompt: String,
// Optional args
// Optional
pub face_count: Option<u32>,
pub generate_type: Option<EnqueueHunyuan3dV3TextTo3dGenerateType>,
pub polygon_type: Option<EnqueueHunyuan3dV3TextTo3dPolygonType>,
pub enable_pbr: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -35,7 +38,7 @@ pub enum EnqueueHunyuan3dV3TextTo3dPolygonType {
Quadrilateral,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueHunyuan3dV3TextTo3dArgs<'_, R> {
impl FalRequestCostCalculator for EnqueueHunyuan3dV3TextTo3dRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Generation types: Normal costs $0.375,
// LowPoly costs $0.45,
@@ -65,7 +68,9 @@ pub async fn enqueue_hunyuan3d_v3_text_to_3d_webhook<R: IntoUrl>(
args: EnqueueHunyuan3dV3TextTo3dArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let generate_type = args.generate_type
let req = args.request;
let generate_type = req.generate_type
.map(|t| match t {
EnqueueHunyuan3dV3TextTo3dGenerateType::Normal => "Normal",
EnqueueHunyuan3dV3TextTo3dGenerateType::LowPoly => "LowPoly",
@@ -73,7 +78,7 @@ pub async fn enqueue_hunyuan3d_v3_text_to_3d_webhook<R: IntoUrl>(
})
.map(|s| s.to_string());
let polygon_type = args.polygon_type
let polygon_type = req.polygon_type
.map(|t| match t {
EnqueueHunyuan3dV3TextTo3dPolygonType::Triangle => "triangle",
EnqueueHunyuan3dV3TextTo3dPolygonType::Quadrilateral => "quadrilateral",
@@ -81,12 +86,12 @@ pub async fn enqueue_hunyuan3d_v3_text_to_3d_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = Hunyuan3dV3TextTo3dInput {
prompt: args.prompt,
prompt: req.prompt,
// Optionals
face_count: args.face_count,
face_count: req.face_count,
generate_type,
polygon_type,
enable_pbr: args.enable_pbr,
enable_pbr: req.enable_pbr,
};
let result = hunyuan3d_v3_text_to_3d(request)
@@ -100,7 +105,7 @@ pub async fn enqueue_hunyuan3d_v3_text_to_3d_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::object::enqueue_hunyuan3d_v3_text_to_3d_webhook::{enqueue_hunyuan3d_v3_text_to_3d_webhook, EnqueueHunyuan3dV3TextTo3dArgs};
use crate::requests::webhook::object::enqueue_hunyuan3d_v3_text_to_3d_webhook::{enqueue_hunyuan3d_v3_text_to_3d_webhook, EnqueueHunyuan3dV3TextTo3dArgs, EnqueueHunyuan3dV3TextTo3dRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -113,13 +118,15 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHunyuan3dV3TextTo3dArgs {
prompt: "A velociraptor with an open mouth full of sharp teeth. Large claws, ready to strike.".to_string(),
face_count: None,
generate_type: None,
polygon_type: None,
request: EnqueueHunyuan3dV3TextTo3dRequest {
prompt: "A velociraptor with an open mouth full of sharp teeth. Large claws, ready to strike.".to_string(),
face_count: None,
generate_type: None,
polygon_type: None,
enable_pbr: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
enable_pbr: None,
};
let result = enqueue_hunyuan3d_v3_text_to_3d_webhook(args).await?;
@@ -5,20 +5,24 @@ use crate::requests::http::object::http_hunyuan3d_v21_image_to_3d::{hunyuan3d_v2
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Hunyuan3d21Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub struct Hunyuan3d21Args<'a, R: IntoUrl> {
pub request: Hunyuan3d21Request,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
pub async fn enqueue_hunyuan_3d_2_1_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan3d21Args<'_, U, V>
#[derive(Clone, Debug)]
pub struct Hunyuan3d21Request {
pub image_url: String,
}
pub async fn enqueue_hunyuan_3d_2_1_image_to_3d_webhook<R: IntoUrl>(
args: Hunyuan3d21Args<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.as_str().to_string();
let req = args.request;
let request = Hunyuan3dV21ImageTo3dInput {
input_image_url: image_url,
input_image_url: req.image_url,
textured_mesh: Some(true),
// TODO: Maybe expose these later
guidance_scale: None,
@@ -38,7 +42,7 @@ pub async fn enqueue_hunyuan_3d_2_1_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::{enqueue_hunyuan_3d_2_1_image_to_3d_webhook, Hunyuan3d21Args};
use crate::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::{enqueue_hunyuan_3d_2_1_image_to_3d_webhook, Hunyuan3d21Args, Hunyuan3d21Request};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -53,7 +57,9 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Hunyuan3d21Args {
image_url: image_url,
request: Hunyuan3d21Request {
image_url: image_url.to_string(),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -5,20 +5,24 @@ use crate::requests::http::object::http_hunyuan3d_v2_image_to_3d::{hunyuan3d_v2_
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Hunyuan3d2Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub struct Hunyuan3d2Args<'a, R: IntoUrl> {
pub request: Hunyuan3d2Request,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
pub async fn enqueue_hunyuan_3d_2_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan3d2Args<'_, U, V>
#[derive(Clone, Debug)]
pub struct Hunyuan3d2Request {
pub image_url: String,
}
pub async fn enqueue_hunyuan_3d_2_image_to_3d_webhook<R: IntoUrl>(
args: Hunyuan3d2Args<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.as_str().to_string();
let req = args.request;
let request = Hunyuan3dV2ImageTo3dInput {
input_image_url: image_url,
input_image_url: req.image_url,
textured_mesh: Some(true),
guidance_scale: None,
num_inference_steps: None,
@@ -7,19 +7,22 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueHailuoV2p3FastProImageToVideoArgs<'a, R: IntoUrl> {
pub request: EnqueueHailuoV2p3FastProImageToVideoRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueHailuoV2p3FastProImageToVideoRequest {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub prompt_optimizer: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueHailuoV2p3FastProImageToVideoArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueHailuoV2p3FastProImageToVideoRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
/// "Your request will cost $0.33 per video."
33
@@ -33,11 +36,12 @@ pub async fn enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueHailuoV2p3FastProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
let req = args.request;
let prompt_optimizer = req.prompt_optimizer.unwrap_or(true);
let request = HailuoV2p3FastProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
prompt: req.prompt,
image_url: req.image_url,
// Optionals
prompt_optimizer: Some(prompt_optimizer),
};
@@ -53,7 +57,7 @@ pub async fn enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook, EnqueueHailuoV2p3FastProImageToVideoArgs};
use crate::requests::webhook::video::image::enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook, EnqueueHailuoV2p3FastProImageToVideoArgs, EnqueueHailuoV2p3FastProImageToVideoRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
@@ -67,9 +71,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHailuoV2p3FastProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
prompt_optimizer: Some(true),
request: EnqueueHailuoV2p3FastProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
prompt_optimizer: Some(true),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,19 +7,22 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueHailuoV2p3ProImageToVideoArgs<'a, R: IntoUrl> {
pub request: EnqueueHailuoV2p3ProImageToVideoRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueHailuoV2p3ProImageToVideoRequest {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub prompt_optimizer: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueHailuoV2p3ProImageToVideoArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueHailuoV2p3ProImageToVideoRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
/// "Your request will cost $0.49 per video generation."
49
@@ -33,11 +36,12 @@ pub async fn enqueue_hailuo_v2p3_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueHailuoV2p3ProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
let req = args.request;
let prompt_optimizer = req.prompt_optimizer.unwrap_or(true);
let request = HailuoV2p3ProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
prompt: req.prompt,
image_url: req.image_url,
// Optionals
prompt_optimizer: Some(prompt_optimizer),
};
@@ -53,7 +57,7 @@ pub async fn enqueue_hailuo_v2p3_pro_image_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_hailuo_v2p3_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_pro_image_to_video_webhook, EnqueueHailuoV2p3ProImageToVideoArgs};
use crate::requests::webhook::video::image::enqueue_hailuo_v2p3_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_pro_image_to_video_webhook, EnqueueHailuoV2p3ProImageToVideoArgs, EnqueueHailuoV2p3ProImageToVideoRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
@@ -67,9 +71,11 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHailuoV2p3ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
prompt_optimizer: Some(true),
request: EnqueueHailuoV2p3ProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
prompt_optimizer: Some(true),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,6 +7,13 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueKling3p0ProImageToVideoArgs<'a, R: IntoUrl> {
pub request: EnqueueKling3p0ProImageToVideoRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueKling3p0ProImageToVideoRequest {
// Request required
pub prompt: String,
pub image_url: String,
@@ -18,10 +25,6 @@ pub struct EnqueueKling3p0ProImageToVideoArgs<'a, R: IntoUrl> {
pub duration: Option<EnqueueKling3p0ProImageToVideoDuration>,
pub aspect_ratio: Option<EnqueueKling3p0ProImageToVideoAspectRatio>,
pub shot_type: Option<EnqueueKling3p0ProImageToVideoShotType>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumIter)]
@@ -92,7 +95,7 @@ pub enum EnqueueKling3p0ProImageToVideoShotType {
Intelligent,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKling3p0ProImageToVideoArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueKling3p0ProImageToVideoRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Same pricing as text-to-video for Kling 3.0 Pro:
// Audio off: $0.224/second
@@ -113,10 +116,12 @@ pub async fn enqueue_kling_3p0_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueKling3p0ProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
let req = args.request;
let duration = req.duration
.map(|d| d.to_str().to_string());
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|ar| match ar {
EnqueueKling3p0ProImageToVideoAspectRatio::Square => "1:1",
EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine => "16:9",
@@ -124,7 +129,7 @@ pub async fn enqueue_kling_3p0_pro_image_to_video_webhook<R: IntoUrl>(
})
.map(|s| s.to_string());
let shot_type = args.shot_type
let shot_type = req.shot_type
.map(|st| match st {
EnqueueKling3p0ProImageToVideoShotType::Customize => "customize",
EnqueueKling3p0ProImageToVideoShotType::Intelligent => "intelligent",
@@ -132,13 +137,13 @@ pub async fn enqueue_kling_3p0_pro_image_to_video_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = Kling3p0ProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
end_image_url: args.end_image_url,
prompt: req.prompt,
image_url: req.image_url,
end_image_url: req.end_image_url,
aspect_ratio,
generate_audio: args.generate_audio,
generate_audio: req.generate_audio,
duration,
negative_prompt: args.negative_prompt,
negative_prompt: req.negative_prompt,
shot_type,
cfg_scale: None,
};
@@ -163,9 +168,7 @@ mod tests {
#[test]
fn test_cost() {
let api_key = FalApiKey::from_str("");
let mut args = EnqueueKling3p0ProImageToVideoArgs {
let mut req = EnqueueKling3p0ProImageToVideoRequest {
prompt: "the t-rex skeleton leaps off the podium and charges".to_string(),
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
end_image_url: None,
@@ -174,32 +177,30 @@ mod tests {
duration: Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds),
aspect_ratio: None,
shot_type: None,
webhook_url: "https://example.com/webhook",
api_key: &api_key,
};
// Audio off: $0.224/sec
// 5s: (224 * 5 + 9) / 10 = 1129 / 10 = 112
assert_eq!(args.calculate_cost_in_cents(), 112);
assert_eq!(req.calculate_cost_in_cents(), 112);
// 10s: (224 * 10 + 9) / 10 = 2249 / 10 = 224
args.duration = Some(EnqueueKling3p0ProImageToVideoDuration::TenSeconds);
assert_eq!(args.calculate_cost_in_cents(), 224);
req.duration = Some(EnqueueKling3p0ProImageToVideoDuration::TenSeconds);
assert_eq!(req.calculate_cost_in_cents(), 224);
// 15s: (224 * 15 + 9) / 10 = 3369 / 10 = 336
args.duration = Some(EnqueueKling3p0ProImageToVideoDuration::FifteenSeconds);
assert_eq!(args.calculate_cost_in_cents(), 336);
req.duration = Some(EnqueueKling3p0ProImageToVideoDuration::FifteenSeconds);
assert_eq!(req.calculate_cost_in_cents(), 336);
// Audio on: $0.336/sec
args.generate_audio = Some(true);
req.generate_audio = Some(true);
// 5s: (336 * 5 + 9) / 10 = 1689 / 10 = 168
args.duration = Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds);
assert_eq!(args.calculate_cost_in_cents(), 168);
req.duration = Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds);
assert_eq!(req.calculate_cost_in_cents(), 168);
// 10s: (336 * 10 + 9) / 10 = 3369 / 10 = 336
args.duration = Some(EnqueueKling3p0ProImageToVideoDuration::TenSeconds);
assert_eq!(args.calculate_cost_in_cents(), 336);
req.duration = Some(EnqueueKling3p0ProImageToVideoDuration::TenSeconds);
assert_eq!(req.calculate_cost_in_cents(), 336);
}
#[tokio::test]
@@ -209,14 +210,16 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKling3p0ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera, then bites".to_string(),
duration: Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: None,
request: EnqueueKling3p0ProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera, then bites".to_string(),
duration: Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -236,14 +239,16 @@ mod tests {
for ar in EnqueueKling3p0ProImageToVideoAspectRatio::iter() {
println!("--- aspect ratio: {:?} ---", ar);
let args = EnqueueKling3p0ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton comes alive and roars at the camera".to_string(),
duration: Some(EnqueueKling3p0ProImageToVideoDuration::ThreeSeconds),
aspect_ratio: Some(ar),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
request: EnqueueKling3p0ProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton comes alive and roars at the camera".to_string(),
duration: Some(EnqueueKling3p0ProImageToVideoDuration::ThreeSeconds),
aspect_ratio: Some(ar),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -263,14 +268,16 @@ mod tests {
for dur in EnqueueKling3p0ProImageToVideoDuration::iter() {
println!("--- duration: {:?} ---", dur);
let args = EnqueueKling3p0ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head and roars".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
request: EnqueueKling3p0ProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head and roars".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -290,14 +297,16 @@ mod tests {
for st in EnqueueKling3p0ProImageToVideoShotType::iter() {
println!("--- shot type: {:?} ---", st);
let args = EnqueueKling3p0ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton lurches forward and snaps its jaws".to_string(),
duration: Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: Some(st),
request: EnqueueKling3p0ProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton lurches forward and snaps its jaws".to_string(),
duration: Some(EnqueueKling3p0ProImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0ProImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: Some(st),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,6 +7,13 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueKling3p0StandardImageToVideoArgs<'a, R: IntoUrl> {
pub request: EnqueueKling3p0StandardImageToVideoRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueKling3p0StandardImageToVideoRequest {
// Request required
pub prompt: String,
pub image_url: String,
@@ -18,10 +25,6 @@ pub struct EnqueueKling3p0StandardImageToVideoArgs<'a, R: IntoUrl> {
pub duration: Option<EnqueueKling3p0StandardImageToVideoDuration>,
pub aspect_ratio: Option<EnqueueKling3p0StandardImageToVideoAspectRatio>,
pub shot_type: Option<EnqueueKling3p0StandardImageToVideoShotType>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumIter)]
@@ -92,7 +95,7 @@ pub enum EnqueueKling3p0StandardImageToVideoShotType {
Intelligent,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKling3p0StandardImageToVideoArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueKling3p0StandardImageToVideoRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Same pricing as text-to-video for Kling 3.0 Standard:
// Audio off: $0.168/second
@@ -113,10 +116,12 @@ pub async fn enqueue_kling_3p0_standard_image_to_video_webhook<R: IntoUrl>(
args: EnqueueKling3p0StandardImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
let req = args.request;
let duration = req.duration
.map(|d| d.to_str().to_string());
let aspect_ratio = args.aspect_ratio
let aspect_ratio = req.aspect_ratio
.map(|ar| match ar {
EnqueueKling3p0StandardImageToVideoAspectRatio::Square => "1:1",
EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine => "16:9",
@@ -124,7 +129,7 @@ pub async fn enqueue_kling_3p0_standard_image_to_video_webhook<R: IntoUrl>(
})
.map(|s| s.to_string());
let shot_type = args.shot_type
let shot_type = req.shot_type
.map(|st| match st {
EnqueueKling3p0StandardImageToVideoShotType::Customize => "customize",
EnqueueKling3p0StandardImageToVideoShotType::Intelligent => "intelligent",
@@ -132,13 +137,13 @@ pub async fn enqueue_kling_3p0_standard_image_to_video_webhook<R: IntoUrl>(
.map(|s| s.to_string());
let request = Kling3p0StandardImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
end_image_url: args.end_image_url,
prompt: req.prompt,
image_url: req.image_url,
end_image_url: req.end_image_url,
aspect_ratio,
generate_audio: args.generate_audio,
generate_audio: req.generate_audio,
duration,
negative_prompt: args.negative_prompt,
negative_prompt: req.negative_prompt,
shot_type,
cfg_scale: None,
};
@@ -163,9 +168,7 @@ mod tests {
#[test]
fn test_cost() {
let api_key = FalApiKey::from_str("");
let mut args = EnqueueKling3p0StandardImageToVideoArgs {
let mut req = EnqueueKling3p0StandardImageToVideoRequest {
prompt: "the t-rex skeleton comes alive and roars".to_string(),
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
end_image_url: None,
@@ -174,32 +177,30 @@ mod tests {
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: None,
shot_type: None,
webhook_url: "https://example.com/webhook",
api_key: &api_key,
};
// Audio off: $0.168/sec
// 5s: (168 * 5 + 9) / 10 = 849 / 10 = 84
assert_eq!(args.calculate_cost_in_cents(), 84);
assert_eq!(req.calculate_cost_in_cents(), 84);
// 10s: (168 * 10 + 9) / 10 = 1689 / 10 = 168
args.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::TenSeconds);
assert_eq!(args.calculate_cost_in_cents(), 168);
req.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::TenSeconds);
assert_eq!(req.calculate_cost_in_cents(), 168);
// 15s: (168 * 15 + 9) / 10 = 2529 / 10 = 252
args.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::FifteenSeconds);
assert_eq!(args.calculate_cost_in_cents(), 252);
req.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::FifteenSeconds);
assert_eq!(req.calculate_cost_in_cents(), 252);
// Audio on: $0.252/sec
args.generate_audio = Some(true);
req.generate_audio = Some(true);
// 5s: (252 * 5 + 9) / 10 = 1269 / 10 = 126
args.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds);
assert_eq!(args.calculate_cost_in_cents(), 126);
req.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds);
assert_eq!(req.calculate_cost_in_cents(), 126);
// 10s: (252 * 10 + 9) / 10 = 2529 / 10 = 252
args.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::TenSeconds);
assert_eq!(args.calculate_cost_in_cents(), 252);
req.duration = Some(EnqueueKling3p0StandardImageToVideoDuration::TenSeconds);
assert_eq!(req.calculate_cost_in_cents(), 252);
}
#[tokio::test]
@@ -209,14 +210,16 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKling3p0StandardImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking toward the camera".to_string(),
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: None,
request: EnqueueKling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking toward the camera".to_string(),
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -236,14 +239,16 @@ mod tests {
for ar in EnqueueKling3p0StandardImageToVideoAspectRatio::iter() {
println!("--- aspect ratio: {:?} ---", ar);
let args = EnqueueKling3p0StandardImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton comes alive and roars at the camera".to_string(),
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::ThreeSeconds),
aspect_ratio: Some(ar),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
request: EnqueueKling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton comes alive and roars at the camera".to_string(),
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::ThreeSeconds),
aspect_ratio: Some(ar),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -263,14 +268,16 @@ mod tests {
for dur in EnqueueKling3p0StandardImageToVideoDuration::iter() {
println!("--- duration: {:?} ---", dur);
let args = EnqueueKling3p0StandardImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head and opens its jaw".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
request: EnqueueKling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton slowly turns its head and opens its jaw".to_string(),
duration: Some(dur),
aspect_ratio: Some(EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(false),
negative_prompt: None,
end_image_url: None,
shot_type: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -290,14 +297,16 @@ mod tests {
for st in EnqueueKling3p0StandardImageToVideoShotType::iter() {
println!("--- shot type: {:?} ---", st);
let args = EnqueueKling3p0StandardImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton lurches forward dramatically".to_string(),
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: Some(st),
request: EnqueueKling3p0StandardImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the skeleton lurches forward dramatically".to_string(),
duration: Some(EnqueueKling3p0StandardImageToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueKling3p0StandardImageToVideoAspectRatio::SixteenByNine),
generate_audio: Some(true),
negative_prompt: None,
end_image_url: None,
shot_type: Some(st),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -6,12 +6,17 @@ use crate::requests::http::video::image::http_kling_v1p6_pro_image_to_video::{kl
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling1p6ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub struct Kling1p6ProArgs<'a, R: IntoUrl> {
pub request: Kling1p6ProRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Kling1p6ProRequest {
pub image_url: String,
pub end_frame_image_url: Option<String>,
pub prompt: String,
pub duration: Kling1p6ProDuration,
pub aspect_ratio: Kling1p6ProAspectRatio,
}
@@ -30,7 +35,7 @@ pub enum Kling1p6ProAspectRatio {
TallNineSixteen, // 9:16
}
impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling1p6ProArgs<'_, U, T, V> {
impl FalRequestCostCalculator for Kling1p6ProRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "Your request will cost $0.095 per second."
match self.duration {
@@ -44,30 +49,27 @@ impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling1p6P
/// Kling 1.6 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v1.6/pro/image-to-video
pub async fn enqueue_kling_v1p6_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling1p6ProArgs<'_, U, T, V>
pub async fn enqueue_kling_v1p6_pro_image_to_video_webhook<R: IntoUrl>(
args: Kling1p6ProArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
let req = args.request;
let duration = match req.duration {
Kling1p6ProDuration::Default => None, // defaults to "5"
Kling1p6ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling1p6ProDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
let aspect_ratio = match req.aspect_ratio {
Kling1p6ProAspectRatio::Square => Some("1:1".to_string()),
Kling1p6ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling1p6ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let tail_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = KlingV1p6ProImageToVideoInput {
image_url,
tail_image_url,
prompt: args.prompt.to_string(),
image_url: req.image_url,
tail_image_url: req.end_frame_image_url,
prompt: req.prompt,
aspect_ratio,
duration,
// Maybe expose these later
@@ -86,7 +88,7 @@ pub async fn enqueue_kling_v1p6_pro_image_to_video_webhook<U: IntoUrl, T: IntoUr
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{enqueue_kling_v1p6_pro_image_to_video_webhook, Kling1p6ProArgs, Kling1p6ProAspectRatio, Kling1p6ProDuration};
use crate::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{enqueue_kling_v1p6_pro_image_to_video_webhook, Kling1p6ProArgs, Kling1p6ProRequest, Kling1p6ProAspectRatio, Kling1p6ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
@@ -100,12 +102,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling1p6ProArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore",
request: Kling1p6ProRequest {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL.to_string(),
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore".to_string(),
duration: Kling1p6ProDuration::Default,
aspect_ratio: Kling1p6ProAspectRatio::WideSixteenNine,
},
api_key: &api_key,
duration: Kling1p6ProDuration::Default,
aspect_ratio: Kling1p6ProAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
@@ -6,11 +6,16 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
pub struct Kling2p1MasterArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub struct Kling2p1MasterArgs<'a, R: IntoUrl> {
pub request: Kling2p1MasterRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Kling2p1MasterRequest {
pub image_url: String,
pub prompt: String,
pub duration: Kling2p1MasterDuration,
pub aspect_ratio: Kling2p1MasterAspectRatio,
}
@@ -29,7 +34,7 @@ pub enum Kling2p1MasterAspectRatio {
TallNineSixteen, // 9:16
}
impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling2p1MasterArgs<'_, U, V> {
impl FalRequestCostCalculator for Kling2p1MasterRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $1.40.
// For every additional second you will be charged $0.28."
@@ -44,26 +49,26 @@ impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling2p1MasterArgs<'_
/// Kling 2.1 Master Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.1/master/image-to-video
pub async fn enqueue_kling_v2p1_master_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling2p1MasterArgs<'_, U, V>
pub async fn enqueue_kling_v2p1_master_image_to_video_webhook<R: IntoUrl>(
args: Kling2p1MasterArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
let req = args.request;
let duration = match req.duration {
Kling2p1MasterDuration::Default => None, // defaults to "5"
Kling2p1MasterDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling2p1MasterDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
let aspect_ratio = match req.aspect_ratio {
Kling2p1MasterAspectRatio::Square => Some("1:1".to_string()),
Kling2p1MasterAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling2p1MasterAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = KlingV2p1MasterImageToVideoInput {
image_url,
prompt: args.prompt.to_string(),
image_url: req.image_url,
prompt: req.prompt,
aspect_ratio,
duration,
// Maybe expose these later
@@ -84,7 +89,7 @@ pub async fn enqueue_kling_v2p1_master_image_to_video_webhook<U: IntoUrl, V: Int
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{enqueue_kling_v2p1_master_image_to_video_webhook, Kling2p1MasterArgs, Kling2p1MasterAspectRatio, Kling2p1MasterDuration};
use crate::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{enqueue_kling_v2p1_master_image_to_video_webhook, Kling2p1MasterArgs, Kling2p1MasterRequest, Kling2p1MasterAspectRatio, Kling2p1MasterDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -99,11 +104,13 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling2p1MasterArgs {
image_url: image_url,
prompt: "a shot of the mountains, the camera rotates around to show the mountain range, the sun begins to set, and the moon becomes visible",
request: Kling2p1MasterRequest {
image_url: image_url.to_string(),
prompt: "a shot of the mountains, the camera rotates around to show the mountain range, the sun begins to set, and the moon becomes visible".to_string(),
duration: Kling2p1MasterDuration::Default,
aspect_ratio: Kling2p1MasterAspectRatio::WideSixteenNine,
},
api_key: &api_key,
duration: Kling2p1MasterDuration::Default,
aspect_ratio: Kling2p1MasterAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
@@ -6,12 +6,17 @@ use crate::requests::http::video::image::http_kling_v2p1_pro_image_to_video::{kl
use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling2p1ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub struct Kling2p1ProArgs<'a, R: IntoUrl> {
pub request: Kling2p1ProRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct Kling2p1ProRequest {
pub image_url: String,
pub end_frame_image_url: Option<String>,
pub prompt: String,
pub duration: Kling2p1ProDuration,
pub aspect_ratio: Kling2p1ProAspectRatio,
}
@@ -30,7 +35,7 @@ pub enum Kling2p1ProAspectRatio {
TallNineSixteen, // 9:16
}
impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling2p1ProArgs<'_, U, T, V> {
impl FalRequestCostCalculator for Kling2p1ProRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $0.45.
// For every additional second you will be charged $0.09."
@@ -44,30 +49,27 @@ impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling2p1P
/// Kling 2.1 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.1/pro/image-to-video
pub async fn enqueue_kling_v2p1_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling2p1ProArgs<'_, U, T, V>
pub async fn enqueue_kling_v2p1_pro_image_to_video_webhook<R: IntoUrl>(
args: Kling2p1ProArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
let req = args.request;
let duration = match req.duration {
Kling2p1ProDuration::Default => None, // Defaults to 5 seconds
Kling2p1ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling2p1ProDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
let aspect_ratio = match req.aspect_ratio {
Kling2p1ProAspectRatio::Square => Some("1:1".to_string()),
Kling2p1ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling2p1ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let tail_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = KlingV2p1ProImageToVideoInput {
image_url,
prompt: args.prompt.to_string(),
tail_image_url,
image_url: req.image_url,
prompt: req.prompt,
tail_image_url: req.end_frame_image_url,
aspect_ratio,
duration,
// Maybe expose these later
@@ -86,7 +88,7 @@ pub async fn enqueue_kling_v2p1_pro_image_to_video_webhook<U: IntoUrl, T: IntoUr
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{enqueue_kling_v2p1_pro_image_to_video_webhook, Kling2p1ProArgs, Kling2p1ProAspectRatio, Kling2p1ProDuration};
use crate::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{enqueue_kling_v2p1_pro_image_to_video_webhook, Kling2p1ProArgs, Kling2p1ProRequest, Kling2p1ProAspectRatio, Kling2p1ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL};
@@ -100,12 +102,14 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling2p1ProArgs {
image_url: SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "a shot of the mountains, the camera pulls back to show a corgi waiting to jump into the lake",
request: Kling2p1ProRequest {
image_url: SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL.to_string(),
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "a shot of the mountains, the camera pulls back to show a corgi waiting to jump into the lake".to_string(),
duration: Kling2p1ProDuration::Default,
aspect_ratio: Kling2p1ProAspectRatio::WideSixteenNine,
},
api_key: &api_key,
duration: Kling2p1ProDuration::Default,
aspect_ratio: Kling2p1ProAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
@@ -7,6 +7,13 @@ use crate::requests::api::webhook_response::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueKlingV2p5TurboProImageToVideoArgs<'a, R: IntoUrl> {
pub request: EnqueueKlingV2p5TurboProImageToVideoRequest,
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Clone, Debug)]
pub struct EnqueueKlingV2p5TurboProImageToVideoRequest {
// Request required
pub prompt: String,
pub image_url: String,
@@ -16,10 +23,6 @@ pub struct EnqueueKlingV2p5TurboProImageToVideoArgs<'a, R: IntoUrl> {
pub negative_prompt: Option<String>,
pub duration: Option<EnqueueKlingV2p5TurboProImageToVideoDurationSeconds>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
@@ -28,7 +31,7 @@ pub enum EnqueueKlingV2p5TurboProImageToVideoDurationSeconds {
Ten,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKlingV2p5TurboProImageToVideoArgs<'_, U> {
impl FalRequestCostCalculator for EnqueueKlingV2p5TurboProImageToVideoRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $0.35.
// For every additional second you will be charged $0.07."
@@ -47,8 +50,10 @@ pub async fn enqueue_kling_v2p5_turbo_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueKlingV2p5TurboProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
// NB: Defaults to 5 seconds
let duration = args.duration
let duration = req.duration
.map(|resolution| match resolution {
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five => "5",
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten => "10",
@@ -56,12 +61,12 @@ pub async fn enqueue_kling_v2p5_turbo_pro_image_to_video_webhook<R: IntoUrl>(
.map(|resolution| resolution.to_string());
let request = KlingV2p5TurboProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
prompt: req.prompt,
image_url: req.image_url,
// Optionals
duration,
tail_image_url: args.tail_image_url,
negative_prompt: args.negative_prompt,
tail_image_url: req.tail_image_url,
negative_prompt: req.negative_prompt,
// Constants
cfg_scale: None,
};
@@ -77,7 +82,7 @@ pub async fn enqueue_kling_v2p5_turbo_pro_image_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs, EnqueueKlingV2p5TurboProImageToVideoDurationSeconds};
use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs, EnqueueKlingV2p5TurboProImageToVideoRequest, EnqueueKlingV2p5TurboProImageToVideoDurationSeconds};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
@@ -91,11 +96,13 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKlingV2p5TurboProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
negative_prompt: None,
tail_image_url: None,
duration: Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five),
request: EnqueueKlingV2p5TurboProImageToVideoRequest {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
negative_prompt: None,
tail_image_url: None,
duration: Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five),
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};

Some files were not shown because too many files have changed in this diff Show More