
    [jT                    `   U 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mZmZmZmZ  ej        e          ZdZded<   d	Zd
Zded<   dZ G d dej                  Zd>dZdddd?dZdddd@dZddddd Zdd!d"d#dAd)Zd*d+ddd,dBd6Zd7d+d+d+d+d8dCd;Z  G d< d=e          Z!dS )Du  
Video Generation Provider ABC
=============================

Defines the pluggable-backend interface for video generation. Providers register
instances via ``PluginContext.register_video_gen_provider()``; the active one
(selected via ``video_gen.provider`` in ``config.yaml``) services every
``video_generate`` tool call.

Providers live in ``<repo>/plugins/video_gen/<name>/`` (built-in, auto-loaded
as ``kind: backend``) or ``~/.hermes/plugins/video_gen/<name>/`` (user, opt-in
via ``plugins.enabled``).

Mirrors the ``image_gen`` provider design (``agent/image_gen_provider.py``) so
the two surfaces stay learnable together.

Unified surface
---------------
One tool — ``video_generate`` — covers **text-to-video** and **image-to-video**.
The router is the presence of ``image_url``: if it's set, the provider routes
to its image-to-video endpoint; if it's omitted, the provider routes to
text-to-video. Users pick one **model family** (e.g. Pixverse v6, Veo 3.1,
Kling O3 Standard); the provider handles which underlying FAL/xAI endpoint
to hit.

Video edit and video extend are intentionally NOT exposed in this surface —
the inconsistency across backends is too large for one unified tool. If
those use cases warrant attention later they can ship as separate tools.

Response shape
--------------
All providers return a dict built by :func:`success_response` /
:func:`error_response`. Keys:

    success         bool
    video           str | None      URL or absolute file path
    model           str             provider-specific model identifier
    prompt          str             echoed prompt
    modality        str             "text" | "image" (which mode was used)
    aspect_ratio    str             provider-native (e.g. "16:9") or ""
    duration        int             seconds (0 if not applicable)
    provider        str             provider name (for diagnostics)
    error           str             only when success=False
    error_type      str             only when success=False
    )annotationsN)Path)AnyDictListOptionalTuple)16:9z9:16z1:1z4:3z3:4z3:2z2:3zTuple[str, ...]COMMON_ASPECT_RATIOSr
   )480p540p720p1080pCOMMON_RESOLUTIONSr   c                      e Zd ZdZeej        d!d                        Zed!d            Zd"dZ	d#d	Z
d$dZd%dZd$dZej        ddddeedddd	d&d             ZdS )'VideoGenProvideru   Abstract base class for a video generation backend.

    Subclasses must implement :meth:`generate`. Everything else has sane
    defaults — override only what your provider needs.
    returnstrc                    dS )zStable short identifier used in ``video_gen.provider`` config.

        Lowercase, no spaces. Examples: ``xai``, ``fal``, ``google``.
        N selfs    :/home/ice/.hermes/hermes-agent/agent/video_gen_provider.pynamezVideoGenProvider.nameR             c                4    | j                                         S )zMHuman-readable label shown in ``hermes tools``. Defaults to ``name.title()``.)r   titler   s    r   display_namezVideoGenProvider.display_nameZ   s     y   r   boolc                    dS )zReturn True when this provider can service calls.

        Typically checks for a required API key and optional-dependency
        import. Default: True.
        Tr   r   s    r   is_availablezVideoGenProvider.is_available_   s	     tr   List[Dict[str, Any]]c                    g S )a  Return catalog entries for ``hermes tools`` model picker.

        Each entry represents a **model family** that supports text-to-video
        and/or image-to-video routing internally::

            {
                "id": "veo-3.1",                       # required
                "display": "Veo 3.1",                  # optional; defaults to id
                "speed": "~60s",                       # optional
                "strengths": "...",                    # optional
                "price": "$0.20/s",                    # optional
                "modalities": ["text", "image"],       # optional, advisory
            }

        Default: empty list (provider has no user-selectable models).
        r   r   s    r   list_modelszVideoGenProvider.list_modelsg   s	    " 	r   Dict[str, Any]c                    | j         ddg dS )z9Return provider metadata for the ``hermes tools`` picker. )r   badgetagenv_vars)r   r   s    r   get_setup_schemaz!VideoGenProvider.get_setup_schemaz   s"     %	
 
 	
r   Optional[str]c                h    |                                  }|r|d                             d          S dS )z7Return the default model id, or None if not applicable.r   idN)r%   get)r   modelss     r   default_modelzVideoGenProvider.default_model   s6    !!## 	'!9==&&&tr   c           	     b    dgt          t                    t          t                    ddddddS )a  Return what this provider supports.

        Returned dict (all keys optional)::

            {
                "modalities": ["text", "image"],      # which inputs the backend accepts
                "aspect_ratios": ["16:9", "9:16", ...],
                "resolutions": ["720p", "1080p"],
                "max_duration": 15,                   # seconds
                "min_duration": 1,
                "supports_audio": True,
                "supports_negative_prompt": True,
                "max_reference_images": 7,
            }

        Used by the tool layer for soft validation and by ``hermes tools``
        for the picker. Default: text-only.
        text
      Fr   )
modalitiesaspect_ratiosresolutionsmax_durationmin_durationsupports_audiosupports_negative_promptmax_reference_images)listr   r   r   s    r   capabilitieszVideoGenProvider.capabilities   s?    ( "(!"677 233#(-$%	
 	
 		
r   N	model	image_urlreference_image_urlsdurationaspect_ratio
resolutionnegative_promptaudioseedpromptrB   rC   rD   Optional[List[str]]rE   Optional[int]rF   rG   rH   rI   Optional[bool]rJ   kwargsr   c       	            dS )u  Generate a video from a prompt (text-to-video) or animate an image
        (image-to-video).

        Routing: if ``image_url`` is provided, the provider should route to
        its image-to-video endpoint; otherwise text-to-video. The plugin
        is responsible for picking the right underlying endpoint within
        the user's chosen model family.

        Implementations should return the dict from :func:`success_response`
        or :func:`error_response`. ``kwargs`` may contain forward-compat
        parameters future versions of the schema will expose —
        implementations MUST ignore unknown keys (no TypeError).
        Nr   )r   rK   rB   rC   rD   rE   rF   rG   rH   rI   rJ   rO   s               r   generatezVideoGenProvider.generate   r   r   r   r   r   r    )r   r#   )r   r&   )r   r-   rK   r   rB   r-   rC   r-   rD   rL   rE   rM   rF   r   rG   r   rH   r-   rI   rN   rJ   rM   rO   r   r   r&   )__name__
__module____qualname____doc__propertyabcabstractmethodr   r   r"   r%   r,   r2   r@   DEFAULT_ASPECT_RATIODEFAULT_RESOLUTIONrQ   r   r   r   r   r   K   s             X ! ! ! X!      &
 
 
 
   
 
 
 
< 	
  $#'48"&0,)- $"       r   r   r   r   c                 `    ddl m}   |             dz  dz  }|                    dd           |S )zBReturn ``$HERMES_HOME/cache/videos/``, creating parents as needed.r   )get_hermes_homecachevideosT)parentsexist_ok)hermes_constantsr_   mkdir)r_   paths     r   _videos_cache_dirrg      sF    000000?w&1DJJtdJ+++Kr   videomp4)prefix	extensionb64_datar   rj   rk   c               2   t          j        |           }t          j                                                            d          }t          j                    j        dd         }t                      | d| d| d| z  }|	                    |           |S )zDecode base64 video data and write under ``$HERMES_HOME/cache/videos/``.

    Returns the absolute :class:`Path` to the saved file.

    Filename format: ``<prefix>_<YYYYMMDD_HHMMSS>_<short-uuid>.<ext>``.
    %Y%m%d_%H%M%SN   _.)
base64	b64decodedatetimenowstrftimeuuiduuid4hexrg   write_bytes)rl   rj   rk   rawtsshortrf   s          r   save_b64_videor~      s     
8
$
$C					 	 	)	)/	:	:BJLLRaR EF!E!ER!E!E%!E!E)!E!EEDSKr   r{   bytesc               
   t           j                                                             d          }t          j                    j        dd         }t                      | d| d| d| z  }|                    |            |S )z@Write raw video bytes (e.g. an HTTP download body) to the cache.rn   Nro   rp   rq   )rt   ru   rv   rw   rx   ry   rg   rz   )r{   rj   rk   r|   r}   rf   s         r   save_bytes_videor      s     
				 	 	)	)/	:	:BJLLRaR EF!E!ER!E!E%!E!E)!E!EEDSKr   webmmovmkv)z	video/mp4z
video/webmzvideo/quicktimezvideo/x-matroskag     f@i  )rj   timeout	max_bytesurlr   floatr   intc          	        ddl }|                    | |d          }|                                 |j                            d          pd                    dd          d                                                                         }t                              |          }|O|                     d	d          d                                         }d
D ]}	|                    d|	           r|	} n|d}t          j	        
                                                    d          }
t          j                    j        dd         }t                      | d|
 d| d| z  }d}|                    d          5 }|                    d          D ]}|s|t%          |          z  }||k    rS|                                 	 |                                 n# t*          $ r Y nw xY wt-          d|  d|dz   d          |                    |           	 ddd           n# 1 swxY w Y   |dk    r9	 |                                 n# t*          $ r Y nw xY wt-          d|  d          |S )a  Download a video URL and write it under ``$HERMES_HOME/cache/videos/``.

    The video twin of :func:`agent.image_gen_provider.save_url_image`: several
    backends (DeepInfra, FAL) return an *ephemeral* delivery URL that expires
    before a downstream consumer can fetch it, so we materialise the bytes
    locally at tool-completion time. Streams with a size cap.

    Raises on any network / HTTP / oversize error so callers can fall back to
    returning the bare URL.
    r   NT)r   streamzContent-Typer(   ;r6   ?)ri   r   r   r   rq   ri   rn   ro   rp   wbi   )
chunk_sizez	Video at z	 exceeds i   zMB cap; refusing to cache.z was empty (0 bytes).)requestsr0   raise_for_statusheaderssplitstriplower_URL_VIDEO_CONTENT_TYPESendswithrt   ru   rv   rw   rx   ry   rg   openiter_contentlencloseunlinkOSError
ValueErrorwrite)r   rj   r   r   r   responsecontent_typerk   url_pathextr|   r}   rf   bytes_writtenfhchunks                   r   save_url_videor      s   " OOO||C|>>H$((88>BEEc1MMaPVVXX^^``L(,,\::I99S!$$Q'--//0 	 	C  S++ 	 						 	 	)	)/	:	:BJLLRaR EF!E!ER!E!E%!E!E)!E!EEDM	4 B**j*AA 	 	E SZZ'My((


KKMMMM   D dddiK.Hddd   HHUOOOO	                	KKMMMM 	 	 	D	?S???@@@KsI   >AH(GH(
G)&H((G))2H((H,/H,:I 
IIr4   r(   )modalityrF   rE   extrarB   rK   r   rF   rE   providerr   Optional[Dict[str, Any]]r&   c           	         d| |||||rt          |          nd|d}|r0|                                D ]\  }	}
|                    |	|
           |S )u  Build a uniform success response dict.

    ``video`` may be an HTTP URL or an absolute filesystem path.
    ``modality`` is ``"text"`` (text-to-video) or ``"image"`` (image-to-video) —
    indicates which endpoint was actually hit, useful for diagnostics.
    Tr   )successrh   rB   rK   r   rF   rE   r   )r   items
setdefault)rh   rB   rK   r   rF   rE   r   r   payloadkvs              r   success_responser   ?  sz    $ $%-4CMMM1	 	G  %KKMM 	% 	%DAqq!$$$$Nr   provider_error)
error_typer   rB   rK   rF   errorr   c           	         dd| |||||dS )z$Build a uniform error response dict.FN)r   rh   r   r   rB   rK   rF   r   r   r   r   r   rB   rK   rF   s         r   error_responser   `  s+      $	 	 	r   c            
          e Zd ZU dZdZded<   dZded<   dZded	<   d
Zded<   d(dZ	d)dZ
d*dZd(dZddddeedddd	d+d'ZdS ), OpenAICompatibleVideoGenProviderug  Generic text/image-to-video over the OpenAI ``client.videos`` API.

    DeepInfra, OpenAI/Sora, and OpenRouter all expose the same
    ``POST /videos`` async-job shape (``create`` → poll → ``download_content``),
    so the SDK call lives here once. A concrete backend only needs to declare
    its identity and credentials::

        class FooVideoGenProvider(OpenAICompatibleVideoGenProvider):
            name = "foo"
            _env_key = "FOO_API_KEY"
            _default_base_url = "https://api.foo.com/v1/openai"
            def list_models(self):
                return [...]   # entries with an "id" key; default_model() uses [0]

    ``image_url`` routes to image-to-video; its absence routes to text-to-video.
    Provider-specific fields (``image_url``/``negative_prompt``/``seed``) ride
    in ``extra_body`` so they pass through the SDK unchanged.
    OPENAI_API_KEYr   _env_keyzhttps://api.openai.com/v1_default_base_urlg      @r   _poll_interval_sg      @_poll_deadline_sr   c                n    dd l }|j                            | j        d                                          S )Nr   r(   )osenvironr0   r   r   )r   r   s     r   _api_keyz)OpenAICompatibleVideoGenProvider._api_key  s/    			z~~dmR0066888r   r    c                D    t          |                                           S N)r    r   r   s    r   r"   z-OpenAICompatibleVideoGenProvider.is_available  s    DMMOO$$$r   clientr   call_kwargsr&   c                   ddl } |j        j        di |}h d}|                                | j        z   }t          |dd          |vr|                                |k    rIt          dt          |dd           dt          | j                   d	t          |dd          d
          |                    | j	                   |j        
                    |j                  }t          |dd          |v|S )aC  Create the video job and poll to completion with a hard deadline.

        Replaces ``client.videos.create_and_poll`` (unbounded 1/s loop) with a
        coarse interval and a wall-clock cap. Returns the terminal video object
        (any status); raises :class:`TimeoutError` if the deadline passes
        first.
        r   N>   r   failedcanceled	cancelled	completed	succeededstatusz
video job r/   r   z( did not reach a terminal status within zs (last status=)r   )timera   create	monotonicr   getattrTimeoutErrorr   sleepr   retriever/   )r   r   r   r   rh   terminaldeadlines          r   _create_and_pollz1OpenAICompatibleVideoGenProvider._create_and_poll  s0    	$$33{33YYY>>##d&;;eXt,,H<<~~8++"Hc!:!: H H%()>%?%?H H$+E8T$B$BH H H  
 JJt,---M**5844E eXt,,H<< r   c                    dd l }|j                            | j                                         dd                                          }|p| j        S )Nr   	_BASE_URLr(   )r   r   r0   r   upperr   r   )r   r   overrides      r   	_base_urlz*OpenAICompatibleVideoGenProvider._base_url  sO    			:>>TY__%6%6"A"A"A2FFLLNN1411r   NrA   rK   rB   r-   rC   rD   rL   rE   rM   rF   rG   rH   rI   rN   rJ   rO   c       	   	        |r|                                 st          dd| j                  S |                                 st          | j         dd| j                  S 	 dd l}n'# t          $ r t          dd| j                  cY S w xY w|p|                                 }|s t          d	| j         d
d| j                  S d ||||
d                                D             }||d}|rt          |          |d<   |r||d<   |r||d<   |
                    |                                 |                                           }	 	 |                     ||          }n# t          $ r{}t                              d| j        d           t          | j         d| d| j        |||          cY d }~t!          |dd           }t#          |          r |             S S d }~ww xY wt!          |dd           }|dvrjt!          |dd           }t          |rt          |          nd|d| j        |||          t!          |dd           }t#          |          r |             S S d }t!          |dd           pg D ]C}t%          |t&                    r|                    d           nt!          |d d           }|r|} nD	 |r$t          t+          || j        !                    }nT|j                            |j                                                  }t          t5          || j        !                    }n# t          $ r}|r$t                              d"| j        |           |}nTt          | j         d#| d$| j        |||          cY d }~t!          |dd           }t#          |          r |             S S Y d }~nd }~ww xY wt7          ||||rd%nd&||pd| j        '          t!          |dd           }t#          |          r |             S S # t!          |dd           }t#          |          r |             w w xY w)(Nzprompt is requiredinvalid_request)r   r   r   z is not setmissing_credentialsr   z8openai Python package not installed (pip install openai)missing_dependencyzno z, video model available (live catalog empty?)no_modelc                    i | ]
\  }}|||S r   r   ).0r   r   s      r   
<dictcomp>z=OpenAICompatibleVideoGenProvider.generate.<locals>.<dictcomp>  s/     	
 	
 	
1 } q }}r   )rH   rF   rC   rJ   )rB   rK   secondssize
extra_body)api_keybase_urlz%s video generation failedT)exc_infoz video generation failed: 	api_errorr   r   r   )r   r   r   zvideo job ended with status=
job_faileddatar   )rj   z3%s: saving video locally failed (%s); returning URLz7 video job succeeded but no output could be retrieved: empty_responseimager4   )rh   rB   rK   r   rF   rE   r   )r   r   r   r   r   openaiImportErrorr2   r   r   OpenAIr   r   	Exceptionloggerdebugr   callable
isinstancedictr0   r   ra   download_contentr/   readr   r   )r   rK   rB   rC   rD   rE   rF   rG   rH   rI   rJ   rO   r   model_idr   r   r   rh   excr   r   	job_errorr   item	candidate	video_refr{   s                              r   rQ   z)OpenAICompatibleVideoGenProvider.generate  s     	V\\^^ 	!*7HSWS\    }} 	!3330   
	MMMM 	 	 	!P/     	 0D..00 	!SDISSS%   	
 	
 $3 ,&	 
 egg	
 	
 	

 19F&K&K 	3%(]]K	" 	-",K 	3(2K%t}}AQAQRRN	--fkBB 	 	 	949tTTT%!YGG#GG*!Y"!!-       N FGT22E U	 UHd33F777 $E7D99	%,5d#i...;dZ`;d;d+!Y"!!-  l FGT22E S Cvt44:  /9$/E/EeDHHUOOO7SWY^`dKeKe	 #CE M #N3ty$I$I$I J JII !-88BBGGIIC #$4S$K$K$K L LI    LL!VX\Xacfggg #II)!%hhcfhh#3!%&%%1       & FGT22E - IIII	 $$-96)!Q   FGT22E  FGT22E s   "A' '!B
BE$ #P $
G).AG$3G)4P $G))AP +AP A:M  ?P  
O
A	O	OP P 	O#P -Q
rR   rS   )r   r   r   r&   r   r   rT   )rU   rV   rW   rX   r   __annotations__r   r   r   r   r"   r   r   r\   r]   rQ   r   r   r   r   r   {  s          & %H$$$$88888 "!!!!#####9 9 9 9
% % % %   02 2 2 2  $#'48"&0,)- $"M M M M M M M Mr   r   )r   r   )rl   r   rj   r   rk   r   r   r   )r{   r   rj   r   rk   r   r   r   )
r   r   rj   r   r   r   r   r   r   r   )rh   r   rB   r   rK   r   r   r   rF   r   rE   r   r   r   r   r   r   r&   )r   r   r   r   r   r   rB   r   rK   r   rF   r   r   r&   )"rX   
__future__r   rZ   rr   rt   loggingrw   pathlibr   typingr   r   r   r   r	   	getLoggerrU   r   r   r  r\   r   r]   ABCr   rg   r~   r   r   r   r   r   r   r   r   r   <module>r     sL  , , ,\ # " " " " " 



           3 3 3 3 3 3 3 3 3 3 3 3 3 3		8	$	$ )\  [ [ [ [ &G  G G G G y y y y ysw y y yB    	     . 	      	   &= = = = = =J &*     H '     6S S S S S'7 S S S S Sr   