
    Pmj                    .   d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZ ddlmZmZmZmZmZ  ej        e          ZdZdZdZd	Zd
Z ej        d          Z ej        d          ZdZddZddZddZ 	 	 dddZ!ddddZ"d Z#d!Z$dZ%d"Z&d#Z'd$Z(d%Z) e*h d&          Z+dd(Z,dd)Z-dd*Z.dd-Z/dd/Z0dd4Z1dd5Z2dd8Z3d9Z4d:Z5d"Z6d;Z7d<Z8dd>Z9dd?Z:dd@Z;ddAZ<ddBZ=ddCZ>dD Z?ddFZ@dG ZAddIZBddKZCddLZDddMZEddNZFddOZGddPZHddRZIddSZJddTZKddUZLddVZMddWZNg dXZOdYgZPdZZQd[ZRd\ZSdd]ZTd^eQd_z   eSz   d` eT            dagdbdcddZUdeeQdfz   eRz   dgz   eSz   d`i  eT            dhdidjdkdlidadhgdbdcddZVdmeQdnz   eRz   dgz   eSz   d`i  eT            dodidjdpdjd dqdrdodsddtdudjd dvdrdwg dxdbdcddZWdyeQdzz   eRz   dgz   eSz   d`i  eT            d{didjd|dlidad{gdbdcddZXd}d~d`didjddldiddddgdbdcddZYddd`i dbdddZZdZ[ ej\        d^eeUeEeNg dd            ej\        deeeVeFeNg dd            ej\        dmeeWeGeNg dd            ej\        dyeeXeHeNg dd            ej\        d}eeYeKeNg dd            ej\        deeZeLeNg dd           dS )u  Native BFL FLUX 3 video generation tools, backed by the Nous tool gateway.

These are native tools in the ``image_generate`` mold: schemas and
descriptions are pinned here as build-time facts, the handlers speak the
gateway's own REST contract, and ``check_fn`` hides the whole toolset only when
there is no Nous sign-in to call it with — never on entitlement, which is the
gateway's to rule on. No runtime discovery, and no server-supplied schema is
ever consulted — that is the point of the design.

The wire is two calls against the gateway's managed mount, and it names the
vendor but not the vendor's API:

- ``POST {base}/generations`` with ``{mode, prompt, ...}`` -> ``{id, status,
  guidance}``
- ``GET {base}/generations/<id>`` -> the job state plus ``guidance``

``guidance`` is the gateway's live policy channel: exact waits, what to do
next, and how to deliver a finished clip ship from the server so they cannot
drift from what it actually enforces. Handlers surface it verbatim as the
tool's result text, and surface ``error.message`` the same way on a refusal.

Media inputs: handlers know their own media fields explicitly. A local file
path is resolved through :func:`tools.image_source.resolve_image_source`
(sandbox confinement, credential guard) and delivered via the Nous upload
protocol (presign, direct PUT to storage, ``nous-upload:<token>`` reference).
URLs pass through untouched.
    )annotationsN)Optional)registry)build_managed_media_uploadermanaged_gateway_auth_headersmanaged_vendor_endpointspeek_nous_access_tokenread_nous_access_tokenbflg     f@g      $@zuBFL video generation needs a Nous Portal sign-in. Ask the user to run `hermes model` and sign in to Nous, then retry.z^[A-Za-z]:[\\/]z!^[A-Za-z0-9+/\r\n]+={0,2}[\r\n]*$   valuestrreturnboolc                f   t          |           t          k    rt                              |           rdS |                     d          rdS | dk    s|                     d          rdS |                     d          rdS t
                              |           s|                     d          rdS dS )u@  True for things we should read off disk rather than forward as-is.

    Deliberately narrow: only explicitly rooted paths and ``file://`` qualify.
    Bare names are ambiguous with opaque ids, URLs pass through, and base64
    payloads (checked first — a JPEG's base64 starts ``/9j/``) fall through
    untouched.
    Fzfile://T~)z~/z~\)/z./z../z.\z..\z\\)len_MIN_BASE64_PAYLOAD_LENGTH_BASE64_PAYLOADmatch
startswith_WINDOWS_DRIVE_PATH)r   s    </home/thesage/.hermes/hermes-agent/tools/flux3_video_tool.py_looks_like_local_pathr   P   s     5zz///O4I4I%4P4P/u	"" t||u''66|t9:: t  '' 5+;+;F+C+C t5    pathc                f    t          |           dk    r| n| dd          dt          |            dS )z=A value safe to embed in an error message shown to the model.   Nu   … (z characters))r   )r   s    r   _display_pathr    e   s;    t99##44D#J)T)TSYY)T)T)TTr   Optional[dict]c                 *    t          t                    S )zCAbsolute URLs for the managed BFL routes, or None when unreachable.)r   _VENDOR r   r   
_endpointsr%   n   s    #G,,,r   methodurl	json_bodyread_timeoutOptional[float]c                  K   ddl }t          |          }|st          j        dt          i          S d|d<   |                    t          |t          n|          }	 |                    |          4 d{V }|	                    | |||           d{V }ddd          d{V  n# 1 d{V swxY w Y   nF# t          $ r9}	t          j        d	t          |	          j         d
|	 dd          cY d}	~	S d}	~	ww xY w|j        dk    rt          j        t          dd          S 	 |                                }
n# t          $ r d}
Y nw xY wt          |
t                    s t          j        d|j         ddd          S |j        dk    rt          |
                    d          t                    r|
                    d          ni }|                    d          p
d|j         d}dt#          |          i}t          |                    d          t                    r|d         |d<   t          j        |d          S |
                    dd          }t          j        |pd|
dd          S )u  One REST round trip, rendered as this tool's result.

    The gateway's ``guidance`` (on success) and ``error.message`` (on a
    refusal) are both written for a model to act on, so they are surfaced
    verbatim. A refusal is a normal outcome the model can respond to — being
    throttled is not a broken tool — so only genuinely unreadable responses
    become ``error``.

    Those unreadable ones carry ``transport_error`` as well. A refusal is the
    gateway's ruling on the request; a transport failure is the absence of one,
    and says nothing about the job. The poll loop tells them apart on that key
    rather than on the wording of a message.
    r   Nerrorzapplication/jsonzContent-Typereadtimeout)headersjsonz.Could not reach the video-generation gateway: : T)r,   transport_errori  )r,   needs_reauthz+The video-generation gateway answered HTTP z with an unreadable body.i  messagez&the gateway refused the request (HTTP )detailsFensure_asciiguidancezRequest accepted.resultr8   )httpxr   r2   dumps_SIGN_IN_MESSAGETimeout"_TRANSPORT_CONNECT_TIMEOUT_SECONDS_TRANSPORT_READ_TIMEOUT_SECONDSAsyncClientrequest	Exceptiontype__name__status_code
isinstancedictgetr   pop)r&   r'   r(   r)   r>   r1   r0   clientresponseexcpayloadr,   r6   outr;   s                  r   _call_gatewayrS   s   s     & LLL*3//G 7z7$456660GNmm*0<0D,,,   G$$W$55 	Z 	Z 	Z 	Z 	Z 	Z 	Z#^^FCy^YYYYYYYYH	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z 	Z   zad3iiFXaa\_aa#
 
   	 	 	 	 	 	 s""z$4dKKLLL--//    gt$$  zr8CWrrr#
 
   	
 s""(27;;w3G3G(N(NVG$$$TV))I&&j*jS[Sg*j*j*jc'll+eii	**D11 	."9-C	Nz#E2222{{:t,,H:22wGG   sT   B> : B,B> ,
B66B> 9B6:B> >
D.C<6D<D,E EE)image)video)input_imageinput_imagesinput_video
   g      n@g      @g      ?g      N@   >   Task not foundContent ModeratedRequest ModeratedErrorReadyrawc                F   	 t          j        |           }n# t          $ r Y dS w xY wt          |t                    rd|v rdS |                    d          }t          |t                    r|                    d          nd}t          |t                     p|t          v S )zETrue when there is nothing to wait for: done, refused, or unreadable.Tr,   r8   statusN)r2   loadsrF   rJ   rK   rL   r   _TERMINAL_POLL_STATUSES)r`   rQ   r8   rb   s       r   _poll_is_finishedre      s    *S//   ttgt$$ 7(:(:
 tkk)$$G&0$&?&?IW[["""TF&#&&&K&4K*KK    
%%c                   	 t          j        |           }n# t          $ r Y dS w xY wt          |t                    rd|vrdS |                    d          }t          |t                    r|                    d          nd}t          |t                    s"t          |t          t          f          r|dk    rdS t          |          S )u  How long the gateway asked us to wait, when a refusal is a throttle.

    A throttle is the one refusal worth absorbing here rather than handing
    back. Returning it ends the call, and the model it lands on has no clock —
    told to wait, it asks again immediately — so a rate limit answered that way
    produces a tighter loop than the one that tripped it. The gateway sends the
    wait as a number alongside the message, so there is nothing to parse out of
    prose.
    Nr,   r8   retryAfterSecondsr   )	r2   rc   rF   rJ   rK   rL   r   intfloat)r`   rQ   r8   r   s       r   _retry_after_secondsrk     s    *S//   ttgt$$ w(>(>tkk)$$G0:7D0I0ISGKK+,,,tE% je&E&E RSt<<rf   c                    	 t          j        |           }n# t          $ r Y dS w xY wt          |t                    o|                    d          du S )zTrue when the gateway did not answer, as opposed to answering "no".

    Set by ``_call_gateway`` on the paths where nothing readable came back, so
    this reads a flag rather than matching on the text of a message.
    Fr4   T)r2   rc   rF   rJ   rK   rL   )r`   rQ   s     r   _is_transport_errorrm   %  s_    *S//   uugt$$O5F)G)G4)OOrf   secondsrj   c                   K   ddl m} | }|dk    rF |            rdS t          t          |          }t	          j        |           d{V  ||z  }|dk    FdS )zFHold the call open until the next look; False if the user interrupted.r   )is_interruptedFNT)tools.interruptrp   min_POLL_WAIT_SLICE_SECONDSasynciosleep)rn   rp   	remaining
this_slices       r   _wait_between_looksrx   2  s      ......I
a--> 	519==
mJ'''''''''Z	 a-- 4r   Nonec                     	 t                       dS # t          $ r&} t                              d|            Y d} ~ dS d} ~ ww xY w)aE  Refresh the Nous token once, before any parallel upload needs it.

    ``read_nous_access_token`` takes no lock and, when a refresh fails, falls
    back to returning the stale cached token. Uploading in parallel therefore
    had every request discover the token was expiring at the same instant and
    fire its own refresh; the rotating refresh token means the first wins and
    the rest quietly send the stale bearer, which the gateway answers with 401.
    One warm-up call first puts a fresh token in the cache, so the fan-out all
    reads it instead of racing for it.
    z5Nous token warm-up failed before parallel uploads: %sN)r
   rF   loggerdebug)rP   s    r   _warm_nous_tokenr}   @  sd    S      S S SLcRRRRRRRRRSs    
A=AargsrK   task_idOptional[str]c           
        K   t          | pi           }t                       t                                          D ]\  }|                    |          }|t          |t                    rtt          |          t          k    r*t          | dt           dt          |           d          t          t          j        fd|D               d{V           ||<   t          |           d{V ||<   |S )u  Replace local paths with upload references in every media field.

    Deliberately covers all media fields rather than the one this mode
    expects. The gateway accepts `input_image` and `input_images`
    interchangeably, so a value left unsanitized in the "wrong" field still
    reaches the vendor — as a raw local path, which both fails the generation
    and discloses the user's directory layout to a third party.
    Nz takes at most z images; you passed .c              3  :   K   | ]}t          |          V  d S N)_deliver_media).0entry	permittedr   s     r   	<genexpr>z!_prepare_media.<locals>.<genexpr>g  s0      &d&dUZ~eY'P'P&d&d&d&d&d&dr   )rK   r}   _MEDIA_FIELDSitemsrL   rJ   listr   _MAX_IMAGES
ValueErrorrt   gatherr   )r~   r   preparedfieldr   r   s    `   @r   _prepare_mediar   Q  s3      DJBH)//11 N NyU##=eT"" 
	N5zzK'' E!h!h+!h!h[^_d[e[e!h!h!hiii #n&d&d&d&d&d^c&d&d&deeeeeee HUOO %35)W$M$MMMMMMMHUOOOr   c                \    d t          | pi                                           D             S )u   Drop media fields entirely — for the mode that takes none.

    The gateway ignores them for text-to-video, so stripping here matches its
    behaviour while avoiding an upload the caller never needed.
    c                ,    i | ]\  }}|t           v||S r$   )r   r   kvs      r   
<dictcomp>z"_without_media.<locals>.<dictcomp>t  s(    PPPTQ-9O9OAq9O9O9Or   rK   r   )r~   s    r   _without_mediar   n  s1     QPT$*"--3355PPPPr   r   tuplec                (  K   t          | t                    rt          |           s| S ddlm}m}m} 	  ||  ||          |           d{V }n0# |$ r(}t          dt          |            d|           |d}~ww xY wt                      }d}	|t          |d         |d	                   }	|	t          d
          	  |	|j        |j                   d{V S # t          $ r(}t          dt          |            d|           |d}~ww xY w)u   Replace a local path with a ``nous-upload:`` reference; pass URLs through.

    Raises ``ValueError`` with a model-readable sentence when the file cannot
    be read or uploaded — the caller turns that into the tool's error payload.
    r   )ImageResolutionErrorResolveContextresolve_image_source)r   )r   NzCould not read r3   base_urlupload_pathzGLocal file uploads are not available in this build; pass a URL instead.zCould not upload )rJ   r   r   tools.image_sourcer   r   r   r   r    r%   r   datamimerF   )
r   r   r   r   r   r   resolvedrP   	endpointsuploaders
             r   r   r   w  s      eS!! )?)F)F ]]]]]]]]]]S--e^^G5T5T5T`ijjjjjjjjj S S SH=+?+?HH3HHIIsRS IH/	*0EyQ^G_``bcccUXhmX];;;;;;;;; U U UJ]5-A-AJJSJJKKQTTUs/   A B #A;;B C 
D)#DDg     r@g      .@i   2   startedc                    t           t          j                    | z
  z
  t          z
  }t	          dt          t          |                    S )ae  What is left of the call for a download, never more than the ceiling.

    Without this the download's own generous timeout outlives the agent's
    per-tool ceiling, and the "saving failed, poll again to retry" answer below
    is never reached: the bridge kills the call first and the model is told
    only "TimeoutError", with no indication the clip exists and is one poll
    away.

    Must not invent time past what remains: clamping upward used to schedule a
    download the outer ``asyncio.wait_for`` then cancelled, answering with a
    false ``_still_generating`` while the job was already Ready.
            )_CALL_BACKSTOP_SECONDStime	monotonic_DOWNLOAD_GRACE_SECONDSmaxrr   _DOWNLOAD_READ_TIMEOUT_SECONDS)r   lefts     r   _download_read_timeoutr     s<     "T^%5%5%?@CZZDsC6==>>>r   c                  K   	 t          j        |           }n# t          $ r | cY S w xY wt          |t                    s| S |                    d          }t          |t                    r|                    d          dk    r| S |                    d          }t          |t                    r|                    d          nd}t          |t                    r|                                s| S |                    dd           	 t          |                                ||           d{V \  }}nK# t          $ r>}	dt          |	          j         d|	 d	|d<   t          j        |d
          cY d}	~	S d}	~	ww xY wt          |          |d<   ||d<   t          |          t          |                    d          pd          z   |d<   t          j        |d
          S )uj  Download a finished clip and swap the signed URL for a local path.

    The URL is handled here rather than by the model on purpose. It is long and
    percent-encoded, and re-keying it into a shell command dropped characters
    often enough to be the main failure mode of this tool — a corrupted
    signature is rejected, but `curl` still writes the rejection body to the
    output file and exits 0, so it read as success. Passing the exact string
    from this response removes the transcription step entirely.

    It also keeps a bearer credential out of the transcript: the signed URL
    grants the clip to anyone holding it for the next 15-60 minutes, and
    returning it put it in the model's context and the saved conversation. The
    gateway already scrubs presigned URLs for *input* media for exactly this
    reason; this makes the output side match.
    r8   rb   r_   r=   sampleNz(The clip finished but saving it failed: r3   zJ. Poll this job again to retry the download; the job itself is unaffected.Fr9   
saved_pathsaved_bytes )r2   rc   rF   rJ   rK   rL   r   striprM   _download_videorG   rH   r?   _delivery_lead_in)
r`   save_tor   rQ   r8   r=   r'   targetsizerP   s
             r   _save_if_readyr     s      *S//   


gt$$ 
kk)$$Ggt$$ H(=(=(H(H
[[""F",VT":":
D&**X


Cc3 syy{{ 
 JJx7,SYY[['7KKKKKKKK 7 7 7WtCyy7I W WS W W W 	 z'6666666667  KKGL!GM)&11CH8M8M8SQS4T4TTGH:gE2222s*    ((
,D7 7
E?3E:4E?:E?c                <    t                      r	d|  d|  dS d|  dS )u  Opens the result text, ahead of the gateway's own delivery guidance.

    On a messaging platform the tag is spelled out rather than described. The
    model has to reproduce this path exactly or the file is not sent, and the
    reply is published either way — the tag is stripped from the text whether
    or not it named a real file, so a wrong path reads to the user as a
    message that simply forgot the attachment. Handing over the finished line
    removes the step where that goes wrong; only this side knows the path.
    z	Saved to z. To deliver it, copy the next line into your reply exactly as written, alone on its own line, with nothing added around it:
MEDIA:
z. )_delivers_as_an_attachment)r   s    r   r   r     sM     "## 
          	

 "v!!!!r   c                  K   ddl }ddlm} t          |t	          |                     }|                    |j        dz             }|                    t          t          |                    }	  ||d          4 d{V }|
                    d|           4 d{V }	|	                                 |                    d	          5 }
|	                                2 3 d{V }|
                    |           6 	 ddd           n# 1 swxY w Y   ddd          d{V  n# 1 d{V swxY w Y   ddd          d{V  n# 1 d{V swxY w Y   |                                j        }|t"          k     rt%          d
| d          |                    |           n%# t(          $ r |                    d            w xY w||fS )a  Stream the clip to disk, returning (path, bytes).

    SSRF-guarded, for the same reason the upload PUT is: this URL comes from
    the vendor by way of the gateway, and it is fetched from the user's own
    machine. Real result URLs are public CDN objects, which the guard allows.
    r   N)create_ssrf_safe_async_clientz.partr-   T)r0   follow_redirectsGETwbzthe download returned only z bytes, which is not a video)
missing_ok)r>   tools.url_safetyr   _resolve_destination_filename_from_url	with_namenamerA   !_DOWNLOAD_CONNECT_TIMEOUT_SECONDSr   streamraise_for_statusopenaiter_byteswritestatst_size_MIN_PLAUSIBLE_VIDEO_BYTESr   replaceBaseExceptionunlink)r'   r   r   r>   r   r   partialr0   rN   rO   handlechunkr   s                r   r   r     sR      LLL>>>>>>!'+=c+B+BCCF v{W455Gmm=DZ[bDcDcmddG00SWXXX 	, 	, 	, 	, 	, 	, 	,\b}}UC00 , , , , , , ,H))+++\\$'' ,6'/';';'='= , , , , , , ,eU++++ (>'=, , , , , , , , , , , , , , ,, , , , , , , , , , , , , , , , , , , , , , , , , , ,	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, ||~~%,,,]4]]]^^^   $''' 4<s   1F) E!*D(D C=&D?D(DD(DD(E(
D2	2E5D2	6E9F) 
EF) EAF) )"Gc                    ddl m} ddlm}m}  | | ||           j                            j        }t          j        dd|          	                    d          d d         }|pdS )	Nr   )PurePosixPath)unquoteurlsplitz[^A-Za-z0-9._-]_r   x   zflux3-video.mp4)
pathlibr   urllib.parser   r   r   r   resublstrip)r'   r   r   r   r   s        r   r   r   $  s    %%%%%%........=#!34455:D6$c40077<<TcTBD$$$r   c                 F    	 ddl m}   |             S # t          $ r Y dS w xY w)a  True on a surface where the clip is received rather than opened off disk.

    Deferred to the shared classifier so this tool cannot drift from the rest
    of the codebase about what counts as a chat channel. It matters here that
    the API server and webhooks are *not* one: they carry a platform value but
    no attachment channel, and neither strips an unfulfilled MEDIA: tag out of
    the reply, so treating them as messaging puts the literal tag in front of
    the caller.
    r   session_is_messaging_surfaceF)gateway.session_contextr   rF   r   s    r   r   r   .  sJ    HHHHHH++---   uus    
  c                 *   ddl m}  t                      r?	 ddlm}  |dd          S # t
          $ r t                              dd           Y nw xY w|                                 d	z  }|	                                r|n| 
                                S )
u  Where a clip lands when the caller named no location.

    On a messaging platform the user has no filesystem — the only way they
    ever see the clip is as an attachment — so it goes to the gateway's own
    video cache, which is an unconditionally allowed delivery root. Downloads
    is not: an operator running HERMES_MEDIA_DELIVERY_STRICT=1 delivers only
    from the cache roots, so a clip saved to Downloads there is dropped on the
    way out and the user is shown a reply with nothing attached.
    r   Path)get_hermes_dirzcache/videosvideo_cachez6Could not resolve the video cache dir; using DownloadsT)exc_info	Downloads)r   r   r   hermes_constantsr   rF   r{   r|   homeis_dircwd)r   r   	downloadss      r   _default_directoryr   @  s     !## b	b777777!>.-@@@ 	b 	b 	bLLQ\`Laaaaa	b		k)I!((**:99

:s   ( &AAfilenamec                   ddl m} t          | t                    r|                                 r~ ||                                                                           }|                                s'|                                                     d          r||}}n|j	        |j
        }}nt                      |}}|                    dd           t          ||z            S )zDWhere to write, honouring an explicit request and never overwriting.r   r   )r   \T)parentsexist_ok)r   r   rJ   r   r   
expanduserr   rstripendswithparentr   r   mkdir
_free_path)r   r   r   	requested	directoryr   s         r   r   r   W  s    '3 9GMMOO 9D))4466	 	?!1!1!:!:;!G!G 	?'tII'.	tII,..4	OOD4O000i$&'''r   c                   |                                  s| S t          dt          dz             D ]@}|                     | j         d| | j                   }|                                 s|c S At          d|            )uG   `name.mp4` -> `name-2.mp4` -> `name-3.mp4` … so nothing is clobbered.   -z'could not find a free filename next to )existsrange_MAX_FILENAME_ATTEMPTSr   stemsuffixr   )	candidater  siblings      r   r  r  h  s     1A566  %%&T&T&&T)BR&T&TUU~~ 	NNN	
JyJJ
K
KKr   r6   c                2    t          j        d| id          S )Nr,   Fr9   r2   r?   )r6   s    r   _errorr  w  s    :w(u====r   modec                j    d t          |pi                                           D             }| |d<   |S )zThe wire body: the model's arguments, minus Nones, plus our mode.

    Arguments pass through as the model gave them; the gateway owns validation
    and the translation onto the vendor's own fields.
    c                    i | ]
\  }}|||S r   r$   r   s      r   r   z _submit_args.<locals>.<dictcomp>  s    GGGTQAqr   r  r   )r  r~   bodys      r   _submit_argsr  {  s>     HGT$*"--3355GGGDDLKr   c                   K   t                      }|t          d          S t          d|d          dt          | |                     d {V S )N4BFL video generation is not available in this build.POSTr   z/generations)r%   r  rS   r  )r  r~   r   s      r   _submitr    sb      ILMMMv)J*?'M'M'M|\`bfOgOghhhhhhhhhr   c                L   K   t          dt          |                      d {V S )Ntext_to_video)r  r   r~   kwargss     r   _handle_text_to_videor     s0      .*>*>?????????r   c                   K   	 t          | |                    d                     d {V }n3# t          $ r&}t          t	          |                    cY d }~S d }~ww xY wt          d|           d {V S )Nr   image_to_videor   rL   r   r  r   r  r~   r  r   rP   s       r   _handle_image_to_videor%    s       'fjj.C.CDDDDDDDD      c#hh )8444444444   ). 
AAAAc                h  K   | pi                      d          }t          |t                    r|st          d          S 	 t	          | |                     d                     d {V }n3# t
          $ r&}t          t          |                    cY d }~S d }~ww xY wt          d|           d {V S )NrW   zKinput_images must be a non-empty list of 1-10 images (local paths or URLs).r   keyframes_to_video)rL   rJ   r   r  r   r   r   r  )r~   r  imagesr   rP   s        r   _handle_keyframes_to_videor*    s      jbn--Ffd## e6 ecddd 'fjj.C.CDDDDDDDD      c#hh -x888888888s   )A+ +
B5BBBc                   K   	 t          | |                    d                     d {V }n3# t          $ r&}t          t	          |                    cY d }~S d }~ww xY wt          d|           d {V S )Nr   video_continuationr#  r$  s       r   _handle_video_continuationr-    s       'fjj.C.CDDDDDDDD      c#hh -x888888888r&  job_idc                B    t          j        d|  d| dddd          S )zHThe backstop's answer: an ordinary "call again", never a raised timeout.u   Still generating. This call reached its own time limit, which the job is unaffected by — call bfl_flux3_get_result again with id=z to keep waiting.
Generating)idrb   r<   Fr9   r  )r.  s    r   _still_generatingr2    sN    :MS   %==	
 	
 
 
 
 
r   c                  K   d}d}	 t          j                    }t          d| t                     d{V }|t          j                    |z
  z  }t	          |          r|dz  }|t
          k    r|S t          }nWd}t          |          }|&t          |          rt          |||           d{V S |t          nt          |t                    }|dk    s||z   t          k    r|S t          |           d{V s|S ||z  })a  Look until the job settles, the budget runs out, or the user stops.

    The waiting is absorbed here rather than asked of the model. A model has no
    clock: told to wait it emits "I'll check back in a minute" and its next
    action lands immediately, so guidance produced a burst of polls rather than
    a paced one. Waiting inside the call cannot be skipped, needs no shell, and
    works the same on every platform.
    r   r   Tr   )r)   N   )r   r   rS   _POLL_READ_TIMEOUT_SECONDSrm   !_MAX_CONSECUTIVE_TRANSPORT_ERRORS_POLL_GAP_SECONDSrk   re   r   r   _POLL_BUDGET_SECONDSrx   )	r'   r   r   spent
unansweredlook_startedr`   gapthrottled_fors	            r   _poll_until_doner>    sS      EJ~''!%;UVVVVVVVVV!!L00s## 	h !OJ>>>
#CCJ055M$):3)?)?$+C'BBBBBBBBB (5'<###mUfBgBgC!88us{&::: J(-------- 	 J=r   c                D  K   | pi                      d          }t          |t                    r|                                st	          d          S t                      }|t	          d          S ddlm} |                                }|d          d ||d	           }| pi                      d
          }t          j	                    }	 t          j        t          |||          t                     d {V S # t          j        $ r t          |          cY S w xY w)Nr1  z9id is required: the job id returned by the generate tool.r  r   )quoter   z/generations/r   )safer   r/   )rL   rJ   r   r   r  r%   r   r@  r   r   rt   wait_forr>  r   TimeoutErrorr2  )r~   r  r.  r   r@  r'   r   r   s           r   _handle_get_resultrD    sZ     jbd##Ffc"" S&,,.. SQRRRILMMM""""""\\^^Fz"
I
IvB1G1G1G
I
ICzry))GnG)%S'733=S
 
 
 
 
 
 
 
 
 	
  ) ) ) ((((()s   /C> >DDc                   K   t           S r   )FLUX3_PROMPTING_GUIDEr  s     r   _handle_prompting_guiderG    s        r   c                    t                      rdS 	 ddlm}   | d          pi }n# t          $ r Y dS w xY w|                    d          }t          |t                    o t          |                                          S )u  True when a Nous bearer is on hand, without spending a refresh to learn it.

    Two lookups, because the transport itself has two.
    ``peek_nous_access_token`` covers the env override and the active store's
    cached token. A profile that was never logged into separately has neither,
    and reads the credential from the global-root ``auth.json`` — the same
    fallback ``resolve_nous_access_token`` takes when the transport refreshes.
    Probing only the first would hide the tools from a profile whose calls
    would have gone through perfectly well.

    Neither lookup validates or refreshes the token: an expired credential is
    the gateway's 401 to report, and that answer already asks for a sign-in.
    Tr   )get_provider_auth_statenousFaccess_token)	r	   hermes_cli.authrI  rF   rL   rJ   r   r   r   )rI  statetokens      r   _has_nous_credentialrO  
  s      t;;;;;;''//52   uuIIn%%EeS!!9d5;;==&9&99s   & 
44c                 b    	 t                      dS t                      S # t          $ r Y dS w xY w)u  Visible to anyone signed in to Nous; the gateway rules on the rest.

    No entitlement check. What an account may generate — plan, credits, per
    account limits — is the gateway's decision, and it refuses with a reason
    written for the model to act on; deciding it a second time here can only
    disagree with the server and hide the tools from someone entitled to them.

    A sign-in is still required, because the gateway takes a Nous bearer and
    nothing else: with no credential every call could only ever answer "sign
    in", so the six schemas would be pure cost on every API call.

    Stays a pair of file reads — no portal probe, no OAuth refresh. Behind the
    registry's 30s cache this still runs on every CLI start, gateway session
    and cron tick.
    NF)r%   rO  rF   r$   r   r   check_bfl_requirementsrQ  $  sD     <<5#%%%   uus       
..)autoz21:9z16:9z4:3z1:1z3:4z9:16z9:21720pz=Read bfl_flux3_prompting_guide before your first generation. zMedia fields accept a local file path (uploaded automatically to Nous-managed temporary storage and deleted when the generation finishes) or a URL. zAAll guidance is defaults: explicit user instructions override it.c                     dddddt          t                    ddddd	d
ddddgddddt          t                    ddddddddddddddddddddS )Nstringr4  u   Generation brief in plain prose (it is interpreted and expanded by a reasoning harness). Order it subject, distinguishing visual specifics, action, camera, lighting, environment, audio, style. Audio is generated by default — say "no music" if unwanted.rG   	minLengthdescriptionrR  z2Output aspect ratio. "auto" lets the model choose.)rG   enumdefaultrX  integer      rG   minimummaximum)rG   constz1Clip duration in whole seconds (5-20), or "auto".)oneOfrZ  rX  rS  zOutput resolution bin.booleanTzGenerate synchronized audio.)rG   rZ  rX  z.Allow a short research pass before generation.r   l    zOptional reproducibility seed.)rG   r_  r`  rX  z(Model version pin. Defaults to "latest".rG   rX  )promptaspect_ratioduration
resolutiongenerate_audio	groundingseedversion)r   _ASPECT_RATIOS_RESOLUTIONSr$   r   r   _shared_submit_propertiesro  K  s     *		
 	
 ((O	
 
 #qR@@!F33 N
 
 &&3	
 
 9
 
 K
 
 !;	
 
 E
 
_3 3 3r   bfl_flux3_text_to_videou^  FLUX 3 text-to-video: generates a clip (with audio) from the prompt alone. Nothing but the prompt anchors the subject here, so research anything with a real, checkable appearance before writing it — whatever you leave unspecified is filled in for you. Generation takes several minutes: this returns a job id immediately; poll bfl_flux3_get_result. objectre  F)rG   
propertiesrequiredadditionalProperties)r   rX  
parametersbfl_flux3_image_to_videou   FLUX 3 image-to-video: animates one image as the literal opening frame — those pixels are frame 0 and the clip moves from there. z-Returns a job id; poll bfl_flux3_get_result. rV   rU  r4  zXExactly one opening-frame image: a local file path or a URL (PNG/JPEG/WebP, up to 10MB).rV  bfl_flux3_keyframes_to_videozFLUX 3 keyframe video: a storyboard of 1-10 images pinned at chosen frame positions (24fps). Name the subject and describe the motion that carries it between the pins, and keep the subject consistent across every pinned image. array)rG   rW  zP1-10 keyframe images: local file paths or URLs (PNG/JPEG/WebP, up to 10MB each).)rG   r   minItemsmaxItemsrX  r[  i  r^  u  One unique non-negative frame index per image (24fps). Each must be at most duration×24, so set an explicit duration rather than "auto" whenever you pin indices — "auto" resolves to 5, 10, 15 or 20 seconds and an index past the length it picks is rejected.)rW   keyframe_indices)re  rW   r{  bfl_flux3_video_continuationu  FLUX 3 video continuation: the new generation picks up from the input clip's final frames. Open the prompt with "Continue this video from its final frames:", re-establish the subject and the moment it ended on, then describe what happens next. input_video must be an mp4 of at most 50MB and 15 seconds, and the generated segment tops out at 15s too — chain a second continuation for a longer sequence. duration is the new segment only. rX   zXThe clip to continue: a local file path or a URL. mp4 only, at most 50MB and 15 seconds.bfl_flux3_get_resulta  Poll a FLUX 3 video job by the job id a generate tool returned. Generation takes minutes and a long Generating phase is normal. This call waits for you while the job runs, so it may run for several minutes; if it returns still generating, just call it again. Do not sleep between calls. On Ready the clip is downloaded for you and the response gives its local path; your only remaining step is to deliver that file as the response describes.z1Job id from a previous bfl_flux3_* generate call.zWhere to save the finished clip: a directory or a full file path. Set this only when the user asked for a particular location; the default is ~/Downloads. An existing file is never overwritten.rd  )r1  r   r1  bfl_flux3_prompting_guidea  Read this before your first FLUX 3 generation. The prompting and grounding guide: how to research a subject so it renders as itself, how to assemble a prompt, which generate tool fits, and how to save and deliver the finished clip. Takes no arguments and spends no generation budget.)rG   rr  rt  uo  # FLUX 3 video generation — how to get the best results

Everything here is guidance, not policy: the user's explicit instructions
always win. If they say skip the research, save somewhere specific, or deliver
differently — do it their way without arguing. Only the server's own validation
and limits are non-negotiable.

Start from the user's own wording. The prompt is rewritten by a reasoning
harness before anything is generated, so restyling it yourself just stacks a
second rewrite on top and gives their intent another chance to drift. There are
two reasons to add anything: grounding facts for a subject with a real,
checkable appearance, and the behaviours below, which they had no way to know
about. Absent those, send what they wrote.

## Grounding

Grounding is your job, and it is the highest-leverage step in the workflow.
What you specify is preserved; what you leave out is filled in for you. So
when a subject has a real, checkable appearance, research it first and put
what you found into the prompt — that is what makes the result yours rather
than an approximation of it.

Ground whenever someone who knows the subject could watch the clip and say
"that's not what that is": a named person or place, a landmark, a particular
vehicle or machine, anything technical, anything culturally specific, or a
period setting. Skip it for generics ("a dog on a beach") — there is no fact
to get wrong.

To ground: search for VISUAL references, where three good photographs beat any
amount of prose. If you can analyze images, analyze what you find. Then put
only what a camera could see into the prompt: silhouette and proportion,
materials and finish, specific colours, distinctive details, era-correct
context.

## How this model behaves

The prompt is read by a reasoning harness rather than a tag encoder, so keyword
tricks and word order do nothing. Write plain prose at whatever length the
brief deserves; a single line is a valid prompt.

Audio is generated by default, whether or not you mention it, so leaving it out
gets you invented sound rather than silence. Name the ambient sound, the music
and any speech separately — each lands as its own layer — and say "no music"
when you do not want it.

A quoted line becomes speech only if a speaker is visible on camera. Without
one it tends to render as burned-in text instead, so quote the line, describe
the speaker, and add "no on-screen text, no subtitles".

Multi-shot sequences work inside a single generation: "SHOT ONE ... HARD CUT.
SHOT TWO ..." produces real cuts, and "one continuous unbroken shot" gets an
uncut take. Consecutive shots have to contrast in scale, location or colour or
the cut will not read as one — near-identical coverage blends back into a
continuous take.

## Choosing a tool

- No input media -> bfl_flux3_text_to_video.
- Animate one image as the literal opening frame ->
  bfl_flux3_image_to_video (those pixels are frame 0, and the clip moves from
  there). Name the subject and its distinguishing specifics here as you would
  anywhere else: only frame 0 is pinned, every frame after it is generated,
  and what you leave out is the model's call rather than yours.
- Several images pinned at chosen moments -> bfl_flux3_keyframes_to_video
  (name the subject and describe the motion that carries it between the pins;
  keep it consistent across every pin, since a mismatch becomes a visible
  morph mid-clip). Pin exact indices and you want an explicit duration too —
  every index must fall within duration×24.
- Keep going from where a clip ends, or chain segments ->
  bfl_flux3_video_continuation. Open with "Continue this video from its final
  frames:", re-establish the subject and the moment the clip ended on, then
  describe what happens next; duration is the new segment only, so plan
  segments of 15 seconds or less to feed outputs back in.

## Media inputs

Pass local file paths directly — never hand-encode file contents into an argument.
URLs also work. Limits per file:
images 10MB (PNG/JPEG/WebP), video one mp4 of at most 50MB and 15 seconds.
Do not pre-shrink files to fit imagined caps; oversized pixel dimensions are
auto-downscaled and output tops out at 720p.

## Workflow

Submit returns a job id immediately — the video does not exist yet. Poll
bfl_flux3_get_result with that id; generation takes several minutes and a long
Generating phase is normal, not a stall. Nothing reaches disk before the job is
Ready, so checking folders mid-run tells you nothing.

The waiting is not yours to do. bfl_flux3_get_result takes the pause itself
while a job is still running, so one call can occupy several minutes and comes
back within seconds of the job finishing. If it returns still generating, just
call it again — no sleeping, no interval to judge, nothing to time.

A job survives client restarts: re-poll the same id rather than resubmitting,
which would only spend your budgets on duplicate work.

## Save and deliver

bfl_flux3_get_result saves the clip itself and returns saved_path. The download
is not yours to do and no URL is handed to you to fetch. Pass save_to only when
the user named a location; otherwise it lands in ~/Downloads, and an existing
file is never overwritten. If saving fails the response says so — poll the same
job again to retry, which is safe and spends no generation budget.

Then deliver that file so the clip plays inline. Which markup plays inline is the
host's decision, so check your system prompt or platform instructions and use
exactly the form they give; the common ones are a MEDIA: tag alone on its own
line and a markdown embed. Two things break it. Write the real absolute path,
with ~ expanded. And keep the markup plain: wrapping it in bold, backticks or a
code fence, or rewriting it as a [link](path), turns an inline player into
literal text or a click-target. That is the most common way this step fails,
and it reads as success because the filename is on screen. Where the
instructions call for no delivery markup, or the host has no such mechanism,
follow them and state the absolute path in plain text.

Report what you did rather than what the job says it did: the echoed prompt
field describes intent and often overstates what the render preserved.
Tu   🎬)r   toolsetschemahandlercheck_fnrequires_envis_asyncemojiu   📖)r   r   r   r   )r   r   r   r   )r   r!   )NN)
r&   r   r'   r   r(   r!   r)   r*   r   r   )r`   r   r   r   )r`   r   r   r*   )rn   rj   r   r   )r   ry   )r~   rK   r   r   r   rK   )r~   rK   r   rK   )r   r   r   r   )r   rj   r   rj   )r`   r   r   rj   r   r   )r   r   )r'   r   r   rj   r   r   )r'   r   r   r   )r   r   )r   r   )r6   r   r   r   )r  r   r~   rK   r   rK   )r  r   r~   rK   r   r   )r~   rK   r   r   )r.  r   r   r   )r'   r   r   rj   r   r   )r   rK   )]__doc__
__future__r   rt   r2   loggingr   r   typingr   tools.registryr   tools.managed_tool_gatewayr   r   r   r	   r
   	getLoggerrH   r{   _TOOLSETr#   rC   rB   r@   compiler   r   r   r   r    r%   rS   r   r   r   r8  r7  rs   r5  r6  	frozensetrd   re   rk   rm   rx   r}   r   r   r   r   r   r   r   r  r   r   r   r   r   r   r   r   r  r  r  r  r   r%  r*  r-  r2  r>  rD  rG  rO  rQ  rm  rn  _GUIDE_POINTER_MEDIA_SENTENCE_OVERRIDE_SENTENCEro  TEXT_TO_VIDEO_SCHEMAIMAGE_TO_VIDEO_SCHEMAKEYFRAMES_TO_VIDEO_SCHEMAVIDEO_CONTINUATION_SCHEMAGET_RESULT_SCHEMAPROMPTING_GUIDE_SCHEMArF  registerr$   r   r   <module>r     s   8 # " " " " "    				        # # # # # #              
	8	$	$
 #( %) "J  !bj!344 
 "*ABB !    *U U U U- - - - !%$(	C C C C C\       
     "  %& ! $)RRR  
L L L L"   .
P 
P 
P 
P   S S S S"   :Q Q Q QU U U UF "' $( !   '   ? ? ? ?"03 03 03 03f" " " "&" " " "J% % % %   $; ; ;.( ( ( ("L L L> > > >   i i i i@ @ @ @5 5 5 59 9 9 99 9 9 9   ) ) ) )X) ) ) )6! ! ! !: : : :4   8 ONNxPC  Y 4 4 4 4p &!	! 	 //11J %	   ( '6	6 	 :		:
 	 
''))
 y 
 
 }- %   4 +B	B 		
 :	: 	 
''))
  "*;;q   "+cJJ3! !
 
 
, CBB %3 & & R +	 	 :	: 		 
''))
 y 
 
 }- %   < #	L  !R  !J 
 
 F %%   B (	 
 $2uUU	 	 "v z  	"!#
	 	 	 	  	# "#
	 	 	 	  	'$&#
	 	 	 	  	'$&#
	 	 	 	  	#
	 	 	 	  	$!##
	 	 	 	 	 	r   