
    ˆj0                    ^   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 ddlmZmZ ddlmZ ddlmZmZ dd	lmZmZmZmZmZmZmZ  ej8                  ej:                  d
        ej<                  d      ZdddddZ de!d<    ejD                         Z#d%dZ$d%dZ%ejL                  d&d       Z' eddde'      Z( G d de      Z)d'dZ*d(dZ+d)dZ,e(j[                  d      d*d       Z.e(j_                  d      d+d       Z0e1d k(  rddl2Z2 e2jf                  d!eed"d#$       yy),uW  server.py - 아누 공용 임베딩 HTTP 서비스 (FastAPI + sentence-transformers)

포트: 8300 (8200=whisper-gpu, 8100=사용중)
모델: jhgan/ko-sroberta-multitask (HF 캐시 로컬, device=cpu, dim=768)

설계 원칙
- 외부 API 키 0. 완전 로컬 추론.
- 모델은 startup(lifespan) 시 **1회만** 로드하여 전역 보관한다. 요청마다 로드 금지.
- 출력 차원 768 불변식은 fail-closed. 다르면 조용히 자르거나 패딩하지 않고 500 으로 실패시킨다.
- GPU 사용 금지 (이 머신 GPU=sm_61, torch 2.10=sm_70+ 만 지원 → CUDA 커널 부재).
    )annotationsN)AsyncGenerator)Any)FastAPIHTTPException)JSONResponse)	BaseModelField)DEVICEEMBEDDING_DIMHOSTMAX_BATCH_SIZEMAX_TEXT_CHARS
MODEL_NAMEPORTz1%(asctime)s [%(levelname)s] %(name)s: %(message)s)levelformatzanu-embedding)model
load_count	loaded_at	dimensionzdict[str, Any]_statec            	     X   ddl m}  t        j                  dt        t
               t        j                         } | t        t
              }t        j                         |z
  }|j                         }|t        k7  r<t        j                  dt        |t               t        dt         d| dt               |t        d	<   t        t        d
         dz   t        d
<   t        j                         t        d<   |t        d<   t        j                  dt        t
        ||t        d
          |S )u   SentenceTransformer 를 CPU 로 1회 로드하고 차원 불변식을 검증합니다.

    Raises:
        RuntimeError: 모델이 보고하는 임베딩 차원이 EMBEDDING_DIM 과 다를 때.
    r   )SentenceTransformeru(   모델 로딩 시작: model=%s device=%s)deviceuH   차원 불변식 위반(모델 로드): expected=%d reported=%s model=%su)   모델 차원 불변식 위반: expected=z
 reported=z model=r   r      r   r   uK   모델 로딩 완료: model=%s device=%s dim=%d elapsed=%.2fs load_count=%d)sentence_transformersr   loggerinfor   r   time get_sentence_embedding_dimensionr   errorRuntimeErrorr   int)r   startedr   elapsedreported_dims        services/embedding/server.py
load_modelr)   @   s     :
KK:JOiikG
6:EiikG#G99;L}$V		
 7jQ]P^^efpeqr
 	
 F7Ovl34q8F<))+F;&F;
KKU| L    c                 `    t         d   } | "t        j                  d       t        dd      | S )u   로드된 전역 모델을 반환합니다. 요청 시점 로딩은 하지 않습니다(fail-closed).

    Raises:
        HTTPException: 503 — 모델이 아직 로드되지 않은 경우.
    r   u:   모델 미로딩 상태에서 요청 수신 — 503 반환i  uF   모델이 로드되지 않았습니다. 서비스 준비 중입니다.status_codedetail)r   r   r"   r   )r   s    r(   	get_modelr/   h   s2     7OE}QR4|}}Lr*   c                  K   	 t                d dt        d<   t        j                  d       y# t        $ r}t        j                  d|        d}~ww xY ww)u\   startup 시 모델을 1회 로드한다. 로드 실패 시 서버는 기동하지 않는다.u    startup 모델 로딩 실패: %sNr   u   서버 종료: 모델 해제)r)   	Exceptionr   	exceptionr   r   )applicationexcs     r(   lifespanr5   z   sP      
F7O
KK./  ;SAs$   A
2 $A	AAAAzANU Embedding Serviceu\   sentence-transformers 기반 로컬 한국어 임베딩 서비스 (외부 API 키 불필요)z1.0.0)titledescriptionversionr5   c                  J    e Zd ZU  edd      Zded<    edd      Zd	ed
<   y)EmbedRequest.u+   임베딩할 텍스트 목록 (1개 이상))r7   	list[str]textsTu   L2 정규화 여부)defaultr7   bool	normalizeN)__name__
__module____qualname__r
   r<   __annotations__r?    r*   r(   r:   r:      s&    S.[\E9\D6KLItLr*   r:   c                l   t        | t              s6t        j                  dt	        |       j
                         t        dd      t        |       dk(  r"t        j                  d       t        dd      t        |       t        kD  rDt        j                  dt        |       t               t        dd	t        |        d
t               g }d}t        |       D ]  \  }}t        |t              s;t        j                  d|t	        |      j
                         t        dd| d      |j                         s't        j                  d|       t        dd| d      t        |      t        kD  r|dz  }|dt         }|j                  |        |rt        j                  d|t               |S )u   입력 텍스트를 검증하고 MAX_TEXT_CHARS 로 truncate 한 목록을 반환합니다.

    Raises:
        HTTPException: 422 — 빈 목록, 비문자열, 전부 공백, 배치 초과.
    u9   입력 검증 실패: texts 가 list 가 아님 (type=%s)i  u.   texts 는 문자열 배열이어야 합니다.r,   r   u-   입력 검증 실패: texts 가 비어 있음uK   texts 가 비어 있습니다. 1개 이상의 문자열이 필요합니다.u-   입력 검증 실패: 배치 초과 (%d > %d)u<   texts 개수가 최대 배치 크기를 초과했습니다: z > uA   입력 검증 실패: texts[%d] 가 문자열이 아님 (type=%s)ztexts[u    ] 가 문자열이 아닙니다.u4   입력 검증 실패: texts[%d] 가 공백 문자열u   ] 가 빈 문자열입니다.r   Nu+   입력 truncate 적용: %d건 (limit=%d자))
isinstancelistr   r"   typer@   r   lenr   	enumeratestrstripr   appendwarning)r<   cleaned	truncatedidxitems        r(   validate_textsrS      s    eT"PRVW\R]RfRfg4dee
5zQDE  5B  C  	C
5zN"Dc%jR`aQRUV[R\Q]]`ao`pq
 	

 GIu% 
	T$$LL\^acghlcmcvcvwC&Ee8fggzz|LLOQTUC&Eb8cddt9~%NI(Dt
 DiQ_`Nr*   c           
     n   	 | D cg c]  }t        |       }}t        |      |k7  r<t        j                  d|t        |             t	        dd| dt        |             g }t        |      D ]  \  }}t        |      t        k7  rHt        j                  d	|t        |      t               t	        dd
| dt        |       dt               	 |D cg c]  }t        |       }}t        d |D              r't        j                  d|       t	        dd| d      |j                  |        |S c c}w # t        $ r)}t        j                  d|       t	        dd      |d}~ww xY wc c}w # t        t        f$ r.}t        j                  d||       t	        dd| d      |d}~ww xY w)u  인코딩 결과의 차원 불변식을 강제합니다 (fail-closed).

    조용한 truncate/padding 을 하지 않고, 위반 시 즉시 500 으로 실패시킵니다.

    Raises:
        HTTPException: 500 — 개수 불일치, 차원 불일치, 비유한값(NaN/Inf) 포함.
    u[   차원 불변식 위반: 인코딩 결과가 순회 가능한 2차원 구조가 아님 (%s)  u5   인코딩 결과 형식이 올바르지 않습니다.r,   NuF   차원 불변식 위반: 벡터 개수 불일치 expected=%d actual=%du,   인코딩 결과 개수 불일치: expected=z actual=uL   차원 불변식 위반: embeddings[%d] dim=%d (expected=%d) — fail-closedu.   임베딩 차원 불변식 위반: embeddings[z] dimension=z
 expected=uL   차원 불변식 위반: embeddings[%d] 에 숫자가 아닌 값 포함 (%s)u4   임베딩 값이 숫자가 아닙니다: embeddings[]c              3  H   K   | ]  }t        j                  |         y w)N)mathisfinite).0vs     r(   	<genexpr>z&validate_embeddings.<locals>.<genexpr>   s     44==##4s    "uJ   차원 불변식 위반: embeddings[%d] 에 NaN/Inf 포함 — fail-closeduS   임베딩에 유한하지 않은 값(NaN/Inf)이 포함되었습니다: embeddings[)rG   	TypeErrorr   r"   r   rI   rJ   r   float
ValueErroranyrM   )	
embeddingsexpected_countrowrowsr4   resultrQ   r[   floatss	            r(   validate_embeddingsrg      s   v%/0cS	00
 4yN"]_morswoxyA.AQQYZ]^bZcYde
 	

 !#FdO Ss8}$LL^C	  DSEVYZ]V^U_ `  -0 	(+,1eAh,F, 4V44LLegjklmplqqrs  	f;> MY 1 vrtwx4klruuv8 -:& 	LLgilnqrMcURST 	sQ   D= D8D= E7E2(E78D= =	E/$E**E/2E77F4)F//F4c                    t               }	 |j                  | |dd      }t        |t        |       	      S # t        $ r  t        $ r,}t        j                  d|       t        dd|       |d}~ww xY w)
uZ   전역 모델로 인코딩한 뒤 불변식 검증을 통과한 벡터를 반환합니다.TF)normalize_embeddingsconvert_to_numpyshow_progress_baru   인코딩 실패: %srU   u   인코딩 실패: r,   N)rb   )r/   encoder   r1   r   r2   rg   rI   )r<   r?   r   rawr4   s        r(   encode_textsrn      s    KEYll!*!#	  
 s3u:>>   Y/56H4NOUXXYs   7 A5	'A00A5z/healthc                    K   t         d   du} t        | rdndt        t        | t        t         d   t         d   t
        t        d	      S w)	u<   서비스 상태와 모델 로딩 상태를 반환합니다.r   Nokloadingr   r   )	statusr   r   loadedr   r   r   max_text_charsmax_batch_sizecontent)r   r   r   r   r   r   r   )rs   s    r(   healthrx     sQ      G_D(F$d)& .,,,

 s   AA
z/embedc                
  K   t        | j                        }t        j                         }t        j                         }t
        4 d{    |j                  dt        || j                         d{   }ddd      d{    t        j                         |z
  }t        j                  dt              | j                  t        |       t        t        t        |d      S 7 7 u7 g# 1 d{  7  sw Y   wxY ww)u   텍스트 목록을 임베딩합니다.

    Request:
        {"texts": ["...", ...], "normalize": true}

    Response:
        {"model": "...", "dimension": 768, "embeddings": [[768 floats], ...]}
    Nu<   임베딩 완료: count=%d normalize=%s dim=%d elapsed=%.3fs)r   r   ra   rv   )rS   r<   r    asyncioget_event_loop_encode_lockrun_in_executorrn   r?   r   r   rI   r   r   r   )requestr<   r%   loopra   r&   s         r(   embedr   )  s      7==)EiikG!!#D ^ ^//lE7K\K\]]
^ ^iikG#G
KKFJ &$
 ^]^ ^ ^ ^s[   AD
C(D&C.4C*5C.9DC,A$D*C.,D.D 4C75D <D__main__z
server:appr   r   )hostport	log_levelworkers)returnr   )r3   r   r   zAsyncGenerator[None, None])r<   r   r   r;   )ra   r   rb   r$   r   list[list[float]])r<   r;   r?   r>   r   r   )r   r   )r~   r:   r   r   )4__doc__
__future__r   rz   
contextlibloggingrX   r    collections.abcr   typingr   fastapir   r   fastapi.responsesr   pydanticr	   r
   settingsr   r   r   r   r   r   r   basicConfigINFO	getLoggerr   r   rC   Lockr|   r)   r/   asynccontextmanagerr5   appr:   rS   rg   rn   getrx   postr   r@   uvicornrunrD   r*   r(   <module>r      s]  
 #      *  * * %     
,,> 
		?	+ 	  w||~%P
$ 	0  	0 
!n	M9 M%P5p?0  $ ( J zGKK r*   