o
    QFjq                     @  s~  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 d dlmZmZ d dlmZmZ d dlmZ d d	lmZmZmZmZmZmZmZ G d
d deZG dd deZ eG dd dZ!dQdRddZ"G dd dZ#G dd dZ$dSd!d"Z%dTd&d'Z&dUd,d-Z'dVd4d5Z(dWd7d8Z)dXd9d:Z*dYd;d<Z+d=d>dZdBdCZ,d[dGdHZ-d\dLdMZ.d]dOdPZ/dS )^    )annotationsN)	dataclass)Path)AnyCallable)	HTTPErrorURLError)Requesturlopen)DEFAULT_FRAMES_PER_ACTION)"_action_visual_review_requirements_build_action_frame_prompt_build_action_strip_prompt_build_canonical_pet_prompt_chat_completions_url_parse_review_content_sha256_textc                   @     e Zd ZdS )OpenAIConfigErrorN__name__
__module____qualname__ r   r   ?/opt/sixxie/releases/current/services/ai/openai_image_client.pyr          r   c                   @  r   )OpenAIRequestErrorNr   r   r   r   r   r      r   r   c                   @  s~   e Zd ZU ded< ded< ded< ded< ded< dZded< d	Zded
< dZded< dZded< edddZ	dddZ
dS )OpenAIConfigstrapi_keybase_urlmodel_imagemodel_image_editmodel_reviewlowquality	1024x1024size      n@floatrequest_timeout_seconds   intmax_retriesreturnc                 C  s
   t | jS N)_mask_secretr   selfr   r   r   masked_api_key.   s   
zOpenAIConfig.masked_api_keyc                 C  s>   d| j d| jd| jd| jd| jd| jd| jdS )	NzOpenAIConfig(api_key=z, base_url=z, model_image=z, model_image_edit=z, model_review=z
, quality=z, size=))r3   r    r!   r"   r#   r%   r'   r1   r   r   r   __repr__2   s    zOpenAIConfig.__repr__N)r.   r   )r   r   r   __annotations__r%   r'   r*   r-   propertyr3   r5   r   r   r   r   r   "   s   
 r   
.env.localenv_path
str | Pathr.   c                 C  s   t t| }i |dd tj D }|dd }|dd d}|dd	 }|d
| }|dd }|rI|rI|rI|rI|sMtdt	||||||dd p\dt
|ddt|ddtdtt|ddd	S )Nc                 S  s    i | ]\}}| d r||qS )OPENAI_)
startswith).0keyvaluer   r   r   
<dictcomp>C   s    z&load_openai_config.<locals>.<dictcomp>OPENAI_API_KEY OPENAI_BASE_URLzhttps://api.openai.com/v1/OPENAI_MODEL_IMAGEgpt-image-2OPENAI_MODEL_IMAGE_EDITOPENAI_MODEL_REVIEWzgpt-4.1-minizOpenAI config is incomplete. Set OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL_IMAGE, OPENAI_MODEL_IMAGE_EDIT, and OPENAI_MODEL_REVIEW.OPENAI_IMAGE_QUALITYr$   OPENAI_IMAGE_SIZEr&   $OPENAI_IMAGE_REQUEST_TIMEOUT_SECONDSr(   r   OPENAI_IMAGE_MAX_RETRIESr+   )	r   r    r!   r"   r#   r%   r'   r*   r-   )_read_env_filer   osenvironitemsgetstriprstripr   r   _normalize_openai_size
_env_floatmaxr,   )r9   
env_valuesmergedr   r    r!   r"   r#   r   r   r   load_openai_config?   s8   
rY   c                   @  s   e Zd ZdZddddeejejddddddXdd Ze		!dYed"dZd&d'Z
d(d)d[d0d1Zd2dd(d3d\d;d<Zd2dd(d=d]d>d?Zd2d@dAd^dCdDZd_dGdHZd`dJdKZdadNdOZdbdRdSZdcdVdWZdS )dOpenAIImageGenerationClientopenairF   r$   r&   Nr(   r+   )r!   r"   r%   r'   openersleepclockboundary_factoryr*   r-   reference_limitr   r   r    r!   r"   r%   r'   r\   Callable[..., Any]r]   Callable[[float], Any]r^   Callable[[], float]r_   Callable[[], str] | Noner*   r)   r-   r,   r`   r.   Nonec                C  s   || _ |d| _|| _|| _|| _|| _t|| _|| _	|| _
|	| _|
p(dd | _tdt|| _tdt|| _tdtt|d| _d S )NrD   c                   S  s   dt  j S )Nz----ai-pet-openai-)uuiduuid4hexr   r   r   r   <lambda>}   s    z6OpenAIImageGenerationClient.__init__.<locals>.<lambda>      @r         )r   rS   r    r!   r"   modelr%   rT   r'   r\   r]   r^   r_   rV   r)   r*   r,   r-   minr`   )r2   r   r    r!   r"   r%   r'   r\   r]   r^   r_   r*   r-   r`   r   r   r   __init__b   s   
z$OpenAIImageGenerationClient.__init__r8   r\   r9   r:   'OpenAIImageGenerationClient'c                C  s2   t |}| |j|j|j|j|j|j||j|jd	S )N)	r   r    r!   r"   r%   r'   r\   r*   r-   )	rY   r   r    r!   r"   r%   r'   r*   r-   clsr9   r\   configr   r   r   from_env   s   z$OpenAIImageGenerationClient.from_envz	1024*1024)r'   pet_namenotesanalysisdict[str, Any]imageslist[dict[str, Any]]c                C  sJ   |st dt|||d}dd t|d | j D }| j|||| jdS )NzDat least one image is required for OpenAI image reference generation)rv   rw   rx   c                 S  s4   g | ]\}}t d d|d  d|d |d dqS )image[]z
reference-rk   z.pngbytesmime
field_namefilenameimage_bytesr~   )_image_part)r=   indeximager   r   r   
<listcomp>   s    zFOpenAIImageGenerationClient.generate_canonical_pet.<locals>.<listcomp>promptimage_partsr'   rm   )
ValueErrorr   	enumerater`   _edit_imager"   )r2   rv   rw   rx   rz   r'   r   r   r   r   r   generate_canonical_pet   s   		z2OpenAIImageGenerationClient.generate_canonical_petr   )candidate_indexprevious_frame_image_bytesr'   actionframe_indexr   canonical_image_bytesr}   r   bytes | Nonec       
      	   C  s   |st d|dk s|tkrt dtd  t|	o|dk}t|||||||d}tdd|d	d
g}|rB|tdd|	p=dd	d
 | j|||
| jdS )Nz8canonical image is required for OpenAI action generationr   "frame_index must be between 0 and rk   walkr   r   r   rv   rw   rx   has_previous_framer|   canonical.png	image/pngr   zprevious-frame.png    r   )r   r   boolr   r   appendr   r"   )r2   r   r   r   rv   rw   rx   rz   r   r   r'   attach_previous_framer   r   r   r   r   generate_action_frame   sH   
z1OpenAIImageGenerationClient.generate_action_frame)r   r   r'   c          
   	   C  sp   |dk s|t krtdt d  t||||||dd}	|r/| j|	tdd|dd	g|| jd
S | j|	|| jdS )Nr   r   rk   Fr   r|   r   r   r   r   )r   r'   rm   )r   r   r   r   r   r"   _generate_imager!   )
r2   r   r   r   rv   rw   rx   r   r'   r   r   r   r   generate_action_frame_minimal   s2   	z9OpenAIImageGenerationClient.generate_action_frame_minimalz1280*720)r   r'   layout_guide_image_bytesc       	         C  s^   |st d|st dt|||||d}
tdd|ddtdd|ddg}| j|
||	| jd	S )
Nz>canonical image is required for OpenAI action strip generationzAlayout guide image is required for OpenAI action strip generation)r   r   rv   rw   rx   r|   zlayout-guide.pngr   r   r   r   )r   r   r   r   r"   )r2   r   r   rv   rw   rx   rz   r   r   r'   r   r   r   r   r   generate_action_strip  s<   z1OpenAIImageGenerationClient.generate_action_stripr   rm   c                C  sJ   |   }||dt|| jd| jd}| | j d|}| j||||dS )Nrk   fallback)rm   r   nr'   r%   z/images/generationsrawrm   r   
started_at)r^   rT   r'   r%   
_post_jsonr    _normalized_result)r2   r   r'   rm   r   payloadr   r   r   r   r   :  s   z+OpenAIImageGenerationClient._generate_imager   c                C  s\   |   }d|fd|fddt|| jdfd| jfg}| j| j d||d}| j||||d	S )
Nrm   r   )r   1r'   r   r%   z/images/edits)fieldsr   r   )r^   rT   r'   r%   _post_multipartr    r   )r2   r   r   r'   rm   r   r   r   r   r   r   r   F  s   z'OpenAIImageGenerationClient._edit_imager   r   c             	   C  s~   t |}t|dtr|di ni }t|}ttd|  | d|d< dd|t|dp5|dp5d	|d
|t|dS )Nusageg           observed_duration_secondsokr[   idcreatedrB   r   )statusproviderrm   
request_idr   
image_mimer   prompt_sha256)	_extract_b64_image
isinstancerQ   dictroundrV   r^   r   r   )r2   r   rm   r   r   r   r   r   r   r   r   R  s    z.OpenAIImageGenerationClient._normalized_resulturlr   c                 C  s  t |d}t| jd D ]}t||dd| j ddd}z'| j|| jd}t 	|
 dW  d    W   S 1 sAw   Y  W q tyu } z"t|jrh|| jk rh| t||d	 W Y d }~qtd
|j |d }~w ty } z|| jk r| d W Y d }~qtd|d }~w ty } z|| jk r| d W Y d }~qtd|d }~w t jy } ztd|d }~ww td)Nutf-8rk   POSTBearer application/jsonAuthorizationzContent-Typedatamethodheaderstimeoutattemptz&OpenAI image request failed with HTTP       ?z:OpenAI image request timed out before receiving a responsez7OpenAI image request failed before receiving a responsez(OpenAI image response was not valid JSON)jsondumpsencoderanger-   r	   r   r\   r*   loadsreaddecoder   _transient_http_statuscoder]   _http_retry_delay_secondsr   TimeoutErrorr   JSONDecodeError)r2   r   r   bodyr   requestresponseexcr   r   r   r   b  sJ   
	*






z&OpenAIImageGenerationClient._post_jsonr   list[tuple[str, str]]c          
      C  s  t |||  d\}}t| jd D ]}t||dd| j |dd}z'| j|| jd}t	|
 dW  d    W   S 1 sDw   Y  W q tyx }	 z"t|	jrk|| jk rk| t|	|d	 W Y d }	~	qtd
|	j |	d }	~	w ty }	 z|| jk r| d W Y d }	~	qtd|	d }	~	w ty }	 z|| jk r| d W Y d }	~	qtd|	d }	~	w tjy }	 ztd|	d }	~	ww td)N)r   r   boundaryrk   r   r   r   r   r   r   r   z+OpenAI image edit request failed with HTTP r   z?OpenAI image edit request timed out before receiving a responsez<OpenAI image edit request failed before receiving a responsez-OpenAI image edit response was not valid JSON)_encode_multipartr_   r   r-   r	   r   r\   r*   r   r   r   r   r   r   r   r]   r   r   r   r   r   )
r2   r   r   r   r   content_typer   r   r   r   r   r   r   r     sR   

	*






z+OpenAIImageGenerationClient._post_multipart)r   r   r    r   r!   r   r"   r   r%   r   r'   r   r\   ra   r]   rb   r^   rc   r_   rd   r*   r)   r-   r,   r`   r,   r.   re   r8   )r9   r:   r\   ra   r.   rq   )rv   r   rw   r   rx   ry   rz   r{   r'   r   r.   ry   )r   r   r   r,   r   r,   rv   r   rw   r   rx   ry   rz   r{   r   r}   r   r   r'   r   r.   ry   )r   r   r   r,   r   r,   rv   r   rw   r   rx   ry   r   r   r'   r   r.   ry   )r   r   r   r,   rv   r   rw   r   rx   ry   rz   r{   r   r}   r   r}   r'   r   r.   ry   )r   r   r'   r   rm   r   r.   ry   )
r   r   r   r{   r'   r   rm   r   r.   ry   )
r   ry   rm   r   r   r   r   r)   r.   ry   )r   r   r   ry   r.   ry   )r   r   r   r   r   r{   r.   ry   )r   r   r   r   r
   timer]   	monotonicro   classmethodru   r   r   r   r   r   r   r   r   r   r   r   r   r   rZ   _   sJ     !9+
-


"rZ   c                   @  sX   e Zd ZdZeddd%ddZe	d&edd'ddZd(ddZd)dd Z	d*d"d#Z
d$S )+OpenAIVisualReviewClientr[   g      N@)r\   r*   r   r   r    rm   r\   ra   r*   r)   r.   re   c                C  s2   || _ |d| _|| _|| _tdt|| _d S )NrD   rj   )r   rS   r    rm   r\   rV   r)   r*   )r2   r   r    rm   r\   r*   r   r   r   ro     s
   	z!OpenAIVisualReviewClient.__init__r8   rp   r9   r:   'OpenAIVisualReviewClient'c                C  s"   t |}| |j|j|j||jdS )N)r   r    rm   r\   r*   )rY   r   r    r#   r*   rr   r   r   r   ru     s   z!OpenAIVisualReviewClient.from_envrv   r   r   r}   r   ry   c          	      C  s   t ||}| jdddddd| d| dt d	t| d
t dt dddd|idgdgdddid}| |}t|}d| j| j|dt|dS )Nsystema-  You review one generated action row for a memorial desktop pet. Return compact JSON only. Check same pet identity, full-body visibility, clean background removal readiness, and action semantics. Reject wrong species, unrelated simplified animals, cropped or broken bodies, or unclear requested action.rolecontentusertextz:Please review one generated action row. Pet display name: z. Action to verify: z. The zU frames must look like the same pet identity and visibly match the action semantics. z Return JSON with keys score (0-1), passed (boolean), action_ok (boolean), identity_ok (boolean), notes (short), risks (array). For walk, also return front_paw_sequence (z$ short strings), hind_paw_sequence (zw short strings), gait_cycle_ok (boolean), two_frame_loop_ok (boolean), and bad_frames (array of 1-based frame numbers).typer   	image_urlr   r   r   r   r   json_objectrm   messagestemperatureresponse_formatr   r   r   r   rm   response_idreview)	_to_data_urlrm   r   r   _post_chat_completions_message_contentr   rQ   r   )	r2   rv   r   r   r   data_urlr   r   r   r   r   r   review_action_frames  sH   
	
#z-OpenAIVisualReviewClient.review_action_framesc                C  s   t ||}| jdddddd| dt dt d	t d
	ddd|idgdgdddid}| |}t|}d| j| j|dt|dS )Nr   a*  You review the final 6-row contact sheet for a memorial desktop pet. Return compact JSON only. Reject packages with wrong-facing frames or action rows that do not match hard required labels. Tail_wag may be a subtle low-disturbance row; do not reject solely because tail_wag resembles idle or look.r   r   r   zSPlease review the final 6-row contact sheet before installation. Pet display name: zo. Rows are fixed: Row 1 idle, Row 2 sleep, Row 3 walk, Row 4 look, Row 5 sit, Row 6 tail_wag. Row 2 sleep: all aB   frames must keep the same sleeping direction/orientation; reject any flipped, opposite-facing, upright, or non-sleeping frame. minor breathing scale changes are not direction flips when the head remains on the same side and the body is not mirrored. Row 3 walk: this project intentionally uses a two-frame walk loop. All z frames must face and walk right in side view; reject any left-facing, front-facing, sitting, or loafing frame. Row 3 frames 1 and 2 must be two distinct walking key poses; Row 3 frames 3 through a{   intentionally repeat Row 3 frames 1 and 2 for looping. Reject if frames 1 and 2 are the same paw stance or only body translation. Row 4 look should use a stable body with head/eye direction changes. Row 6 tail_wag may be subtle; a small visible tail or rear-body variation is acceptable for a calm desktop pet. Do not reject solely because tail_wag resembles idle or look when identity, full-body visibility, walk direction, and sleep direction are acceptable. Also check same pet identity, full-body visibility, calm low-disturbance tone, and no text/scenery. Return JSON with keys score (0-1), passed (boolean), identity_ok (boolean), row_semantics_ok (boolean), direction_ok (boolean), row_distinct_ok (boolean), gait_cycle_ok (boolean for Row 3 walk), two_frame_loop_ok (boolean for Row 3 walk), repair_actions (array of action names needing regeneration), notes (short), risks (array).r   r   r   r   r   r   r   r   r   r   r   )r   rm   r   r   r   r   rQ   r   )r2   rv   r   r   r   r   r   r   r   r   r   review_contact_sheet  s@   
		)
.z-OpenAIVisualReviewClient.review_contact_sheetr   c              
   C  s  t |d}tt| j|dd| j ddd}z&| j|| jd}t 	|
 dW  d    W S 1 s9w   Y  W d S  tyU } z	td|j |d }~w tye } ztd	|d }~w tyu } ztd
|d }~w t jy } ztd|d }~ww )Nr   r   r   r   r   r   r   z'OpenAI review request failed with HTTP z;OpenAI review request timed out before receiving a responsez8OpenAI review request failed before receiving a responsez)OpenAI review response was not valid JSON)r   r   r   r	   r   r    r   r\   r*   r   r   r   r   r   r   r   r   r   )r2   r   r   r   r   r   r   r   r   r   C  s4   
	(


z/OpenAIVisualReviewClient._post_chat_completionsN)r   r   r    r   rm   r   r\   ra   r*   r)   r.   re   r   )r9   r:   r\   ra   r.   r   )
rv   r   r   r   r   r}   r   r   r.   ry   )rv   r   r   r}   r   r   r.   ry   )r   ry   r.   ry   )r   r   r   r   r
   ro   r   ru   r   r   r   r   r   r   r   r     s    

6@r   r   r   r   r   r}   r~   ry   c                 C  s.   t |tr|st| d| |||pddS )Nz image bytes are requiredr   )r   r   r}   r~   )r   r}   r   r   r   r   r   r   [  s   r   status_coder,   r   c                 C  s   t | dv S )N>                 )r,   )r   r   r   r   r   f  s   r   r   r   r   r)   c                C  sv   d}t | dd }|d urt|ddpd }z|r&tdtt|dW S W n	 ty0   Y nw tdtd| dS )NrB   r   zRetry-Afterr   g      $@g       @r   )getattrr   rQ   rR   rV   rn   r)   r   )r   r   retry_afterr   r   r   r   r   j  s   r   r   r   r   r{   r   tuple[bytes, str]c              	   C  s   g }| D ] \}}| d| ddd| ddt|ddg q|D ],}| d| ddd|d  d	|d
  ddd|d  dd|d dg q'|d| dd d|d| fS )Nz--z
asciiz&Content-Disposition: form-data; name="z"

r   s   
r   z"; filename="r   z"
zContent-Type: r~   z

r}   z--
r   zmultipart/form-data; boundary=)extendr   r   r   join)r   r   r   chunksnamer?   partr   r   r   r   w  s.   r   r   c              
   C  sx   |  d}t|tr8|r8t|d tr|d  dnd }|r8ztt|W S  ty7 } ztd|d }~ww td)Nr   r   b64_jsonz9OpenAI image response contained invalid base64 image dataz9OpenAI image response did not include b64_json image data)	rQ   r   listr   base64	b64decoder   r   r   )r   r   r  r   r   r   r   r     s   
 
r   c                 C  s   |  di gd  di  dd}t|tr<g }|D ]}t|tr/d|v r/|t|d  q|t| qd|S t|S )Nchoicesr   messager   rB   r   
)rQ   r   r  r   r   r   r  )r   r   partsr  r   r   r   r     s   "

r   c                 C  s   d| dt | d S )Nzdata:z;base64,r  )r  	b64encoder   )r   r~   r   r   r   r     s   r   r&   r   r'   
str | Noner   c                C  s   t | pd  dd}|dv r|S d|v rD|dd\}}z
t|}t|}W n ty5   | Y S w ||kr<dS ||krBdS dS |S )	NrB   *x>   autor&   	1024x1536	1536x1024rk   r  r  r&   )r   rR   lowerreplacesplitr,   r   )r'   r   clean
width_textheight_textwidthheightr   r   r   rT     s"   rT   pathr   dict[str, str]c                 C  sv   |   si S i }| jdd D ](}| }|r!|ds!d|vr"q|dd\}}| dd|| < q|S )Nr   )encoding#=rk   "')exists	read_text
splitlinesrR   r<   r"  )r(  valuesraw_lineliner>   r?   r   r   r   rM     s   rM   r2  r>   defaultc              	   C  s6   zt t| || W S  ttfy   | Y S w r/   )r)   r   rQ   rR   	TypeErrorr   )r2  r>   r5  r   r   r   rU     s
   rU   r?   c                 C  s.   t | dk rdS | d d  d| dd   S )N   z***r+   z...)len)r?   r   r   r   r0     s   r0   r   )r9   r:   r.   r   )
r   r   r   r   r   r}   r~   r   r.   ry   )r   r,   r.   r   )r   r   r   r,   r.   r)   )r   r   r   r{   r   r   r.   r
  )r   ry   r.   r}   )r   ry   r.   r   )r   r}   r~   r   r.   r   )r'   r  r   r   r.   r   )r(  r   r.   r)  )r2  r)  r>   r   r5  r)   r.   r)   )r?   r   r.   r   )0
__future__r   r  r   rN   r   rf   dataclassesr   pathlibr   typingr   r   urllib.errorr   r   urllib.requestr	   r
   packages.pet_package_schemar    services.ai.apimart_image_clientr   r   r   r   r   r   r   RuntimeErrorr   r   r   rY   rZ   r   r   r   r   r   r   r   r   rT   rM   rU   r0   r   r   r   r   <module>   sD    $   N 
1







