From fc25a47f04e30014a6209d4f135d9c875654e99a237dd4418e080cf8d35a121b Mon Sep 17 00:00:00 2001 From: lansan69 Date: Tue, 31 Mar 2026 02:25:58 -0600 Subject: [PATCH 1/2] =?UTF-8?q?M=C3=B3dulo=20de=20audio=20y=20avance=20im?= =?UTF-8?q?=C3=A1genes=20(schemas=20y=20openAI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__pycache__/__init__.cpython-312.pyc | Bin 167 -> 152 bytes app/__pycache__/main.cpython-312.pyc | Bin 2373 -> 2358 bytes .../v1/__pycache__/__init__.cpython-312.pyc | Bin 203 -> 188 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 215 bytes .../__pycache__/router.cpython-312.pyc | Bin 1977 -> 1962 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 221 bytes .../__pycache__/transcription.cpython-312.pyc | Bin 2089 -> 2074 bytes .../rubricated_analysis.cpython-312.pyc | Bin 2118 -> 2138 bytes .../v1/endpoints/image/rubricated_analysis.py | 2 + .../texto/__pycache__/resume.cpython-312.pyc | Bin 2047 -> 2032 bytes .../rubricated_analysis.cpython-312.pyc | Bin 2146 -> 2131 bytes .../__pycache__/transcription.cpython-312.pyc | Bin 1984 -> 1969 bytes app/core/__pycache__/config.cpython-312.pyc | Bin 784 -> 852 bytes app/core/config.py | 2 +- .../__pycache__/audio_standar.cpython-312.pyc | Bin 1751 -> 0 bytes .../audio_standard.cpython-312.pyc | Bin 4631 -> 4424 bytes .../image_standard.cpython-312.pyc | Bin 2518 -> 4460 bytes .../__pycache__/text_standard.cpython-312.pyc | Bin 2495 -> 2480 bytes .../video_standard.cpython-312.pyc | Bin 3446 -> 3431 bytes app/schemas/audio_standard.py | 24 +-- app/schemas/image_standard.py | 54 +++--- .../transcription_adapters.cpython-312.pyc | Bin 6883 -> 0 bytes .../transcription_adapters.cpython-312.pyc | Bin 7778 -> 12573 bytes app/services/audio/transcription_adapters.py | 82 ++++++++- .../evaluations_adapters.cpython-312.pyc | Bin 2674 -> 6684 bytes .../prompt_builder.cpython-312.pyc | Bin 0 -> 3484 bytes app/services/image/evaluations_adapters.py | 130 ++++++++++++-- app/services/image/prompt_builder.py | 57 +++++++ .../evaluations_adapters.cpython-312.pyc | Bin 2117 -> 2613 bytes .../resume_adapters.cpython-312.pyc | Bin 2115 -> 2100 bytes app/services/text/evaluations_adapters.py | 19 ++- .../transcription_adapters.cpython-312.pyc | Bin 2429 -> 2518 bytes app/services/video/transcription_adapters.py | 37 ++-- app/tests/id.json | 161 ++++++++++++++++++ .../audio_utilities.cpython-312.pyc | Bin 2468 -> 2453 bytes .../image_utilities.cpython-312.pyc | Bin 0 -> 2767 bytes app/utilities/image_utilities.py | 52 ++++++ 37 files changed, 538 insertions(+), 82 deletions(-) create mode 100644 app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc create mode 100644 app/api/v1/endpoints/audio/__pycache__/__init__.cpython-312.pyc delete mode 100644 app/schemas/__pycache__/audio_standar.cpython-312.pyc delete mode 100644 app/services/__pycache__/transcription_adapters.cpython-312.pyc create mode 100644 app/services/image/__pycache__/prompt_builder.cpython-312.pyc create mode 100644 app/services/image/prompt_builder.py create mode 100644 app/tests/id.json create mode 100644 app/utilities/__pycache__/image_utilities.cpython-312.pyc create mode 100644 app/utilities/image_utilities.py diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc index f3f153baebcbaf38af77335cfa537aeb77c2a8150d780b0f1a1c1f257284e50b..62f1043d9f5e03757bf2fb4f4fef0f1ec50db9e3d9d94c6be883e7d550adb7db 100644 GIT binary patch delta 82 zcmZ3^ID?V*G%qg~0}vc-Ii1Nkk=M}DTt6c}H&s6;F|Rl=&&*Q4IJKxOGdVL~KQmD` gH#4~?A0m*dpIA_!A0MBYmst`YuUAm{Yhs%s06J?N$^ZZW delta 97 zcmbQixSWyqG%qg~0}!0twl$M!BCnx`o3m9+XmM&$aZEvCa&~@qv`vWjG%qg~0}vc-Ii0Dvk#`jnX5l008dP9#8-P delta 101 zcmdlcbX17B^G=4F<|$LkeT-kLZ= F1psxVCcOXv diff --git a/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..ee3e21fa5b74dfd7bcbde45b42af1b0b7aecc0c0d956c2300afa84a756cee89a GIT binary patch literal 215 zcmX@j%ge<81jnVfW{Lvo#~=<2FhUuhIe?7m3@Hpz43&(UOjUf20iFtFh6)9V$=QkN zseYP_x7g$3Q}UDJ<5x0#25I^gsh^Rbo2s9am{**bXJ)BioLW?tnVgxgUs#%$lbMoV zqFYd@3)E4ppO#o$;#iOwlA2qPlUS0fpIA@;#F_eKhWe>_DFykNc_qdA@$s2?nI-Y@ ndIgogIBatBQ%ZAE?TXldHi6t&407KGW=2NFTMQyaEI_DFykNc_qdAiKQt(z47sx sd6^~g@p=W7zc_4i^HWN5QtgV^fcAl0S`2dK2WCb_##;;`MJzxL0Of%_=l}o! literal 0 HcmV?d00001 diff --git a/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc b/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc index 695a164ffd103b4f47855313943cb95d17d9caa63a7f9e525146aae70337f8ab..8b36043228d10ff7acb5821cf622bceb72b65d244caee399da8b274ebc746352 100644 GIT binary patch delta 102 zcmZ1}FiU{T3vdrYneErNs y-Q3LNqI`%zs(xZY0T5^Eml^7(=A{(mXXcd@>nE0`0F`d$Wt+{&_-pb?_67hD3?y~{ delta 117 zcmbOwuu_2cG%qg~0}w2V-#N_P!828K)kJ6-=!qUW? z%#{2R-GWNpl>Fr4n6$*=632qfkks6QoWzpUn8bntAkK^_GmJ^iODV|D%quC5Ni0pt S%#Ydpk99U9A(}&i1eSdlM z{-4a_!MCxC$|$Bg@N-8OvTNf&hH~a|&&1J04s{l{IUj zs!d97=di=mTqLpbjJC2|EmsJLu#~&LdZns&|X}3$14^k2Llx5~3mgGF{ zE87U%4YOSPDMu@X46uP~;1yLoGjA4O*7e+$u50yITQ76fc<})*q6t&$7hSvptHoFR zcEkMP9NcRHHLLOHZpwrmM0`s%wbc6oSx@7y1tGkvxX^y61K79)uZ!P!*M6r{{{V6H BctZdH delta 496 zcmYk2PfHs?7{+I^e`XW2Nt85IXh;Ml?|{>E(`*?lkW@?e%+F6A zWW;sB!*w$g9Qj}*HT4XlEWtwRproN-Di8%LZIIfdJd{}#qPj5G9c^^yQL>P}CQWle z%@)%!>0moOC96+W>>0WSBDoC6tSu3;VZfiub&&8=C}x054P$(jv#@?sMY!?JdatiR F`#%Ucc@qEt diff --git a/app/api/v1/endpoints/image/rubricated_analysis.py b/app/api/v1/endpoints/image/rubricated_analysis.py index cafc0ab..7a0086e 100644 --- a/app/api/v1/endpoints/image/rubricated_analysis.py +++ b/app/api/v1/endpoints/image/rubricated_analysis.py @@ -8,6 +8,8 @@ Homologación: Sin importar qué proveedor de IA se utilice, el resultado siempre se entrega en el mismo formato estándar para Qualidot. """ +from typing import Any + from dotenv import load_dotenv from fastapi import APIRouter, Depends, UploadFile, File, HTTPException from fastapi.security import APIKeyHeader diff --git a/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc b/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc index 2329166c4516402d6b151002760b9351311c3d41dcc386c5dea1140215d20781..fa6687b524980da89af09a7900e1338a3891e53502e15489fae6a40f8acdc9a9 100644 GIT binary patch delta 102 zcmey*|AC+PG%qg~0}vc-Ii0y{Bkw#`7a#qM{M=OioW#81#5^-g{o>T3vdrYneErNs z-Q3LNqI`%zs(xZY0T5^Eml^7(=A{(mXXcd@>zAZfl;rDg{>R$L$oOk=1A9FHVLT=I delta 117 zcmeys|DT`tG%qg~0}w2V-#N_P!828K)kJ6-=!qUW? z%#{2R-GWNpl>Fr4n6$*=632qfkks6QoWzpUn8bntAkK^_GmJ^iODV|D%quC5DM_s; S$&cCmp0$yY@z&%*_Id#P2Q2si diff --git a/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc b/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc index 8564fc5be5ec55cca540108b273dd773f77e32c40bed9a964d0fb32e14920dd0..f40e7e3e16d3938295d7a12d961f5a63592da3112c7a59b1090b12928278bfd7 100644 GIT binary patch delta 102 zcmaDPa9M!&G%qg~0}vc-Ih`rIk@r2TOQe2Aer~FMPGVkhVxF0$esOA1S!Qx(zJ6w+ zZf<6BQ9eWL(Ty=qKkFr2>h(w9It9g32ORpaEcl4ag|Ym~77! zlz5rN_5&A#gyMvv>6H^JFDM#s2)Zn8agjx&NEoJCleLHs$iBsvl3$XVS60LfleooE zoLW+nnU`K%Bm@!z>kt95esS33=BJeAq}mmUPrkz>Cnm&b`H=xcXfW`IbXzrYfYky3 DMTk-- delta 222 zcmcb@Hi3=zG%qg~0}!~J*qX^bk#`c~yon3uvsAKbvQ5rpbYk|?n&Ma3}%iOJddG47cq9;Hb!g{6r(nJM`tx&@WGDf!98F=>g#C5{D|A*s0q zIf*5yF^L5QG0FKwsX!txEi+xOpt6V+Xdakg12T$(Cg(B*30`5bEfRu>YqA#cPJYIu vASDPA0SgNQS+_WBa`RJ4b5iY!#3m~+%Zc$a+I?gI5gH6UBHdPv9ALEo^ann> diff --git a/app/core/config.py b/app/core/config.py index 1f7d4c1..48d69fc 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -21,6 +21,6 @@ class Settings: # --------------------------------------------------------------- # Proveedores de Imagen # --------------------------------------------------------------- - + CLARIFAI_API_KEY = os.getenv("CLARIFAI_API_KEY", "") settings = Settings() \ No newline at end of file diff --git a/app/schemas/__pycache__/audio_standar.cpython-312.pyc b/app/schemas/__pycache__/audio_standar.cpython-312.pyc deleted file mode 100644 index 81a07eeb13a8875ee9b57f88702123cda02394f13621e926d952c030d187cc54..0000000000000000000000000000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1751 zcmZ`(&1+pn6rcP3-c8b^rg`S0)(cYcLh^Pl3`K}vsi=uI4d~M)rxfdH&=N?DXjgt_n&83>*Q{P022wCzttnjJonJWrQx2gZ9WjjH)FlMQ!oG;8(MTZD}L3z69@Px9AC6u!yXE6#07CD(nuQNs= z7%cs8#VK+g@f&asDD`q0r%}4)SEcu>UG?ESIV^CX1z7Z`wyIeFne#f5vp5sXk+3+) z1>3@132uzJjMJ*d=g*x%7U#3Vk2p>9ru`3IfUA`DW)`JB{eVZzRZAUbkkB>o)!*?Y z`vm(AN^L#Md7Ai9fIEl78i3x%0QLwYEd^Mks)T7&Ie?4B0h<0IE!3#W45~4c>dXvH zYU~;5*f9E7Wvx@1xKj9Fqzq}qQQ?=H7DB@?op`N217 zdzI&xi?7?;gHA7Hoh6zEvK{&|U&;7YxVy;DS=-ODwhXQT=ViO1lP7Z@8QRP`WzF+= z!gJ5NG51^(hOTL-K+b`{%~e)|Fs{!IH}i+g}=_s9vd(pn`F9iY$2+Vsp-e)>xadNPcaI!cc{UY%iv#sNq#yRHoETEjD2CLp#ozUFcdqRhchf)g#ePJL zv*wPMmgfO$fg~34*3WpK6=w!o6MvIfo{|RMWQXD}76=nUlxE(^c(Th}U diff --git a/app/schemas/__pycache__/audio_standard.cpython-312.pyc b/app/schemas/__pycache__/audio_standard.cpython-312.pyc index b95addac46dd0f79e64520dec3d9667b8a2f7a982b83b38cee2e54e6297e587a..f4b703fd8681ba74ecb3af4ccf14d90f9fe053391191ea944175943be69b053d 100644 GIT binary patch delta 1312 zcmZ{j&u<$=6vt=Q>-}Z#+D;wf0u7Ey2oA(P5DEzwE3k}0{eDaUrwQC!tjUCq>DTXIrv z+DyB;sbfS10lkGRc?(&}z5pc=+8<5GG~$@bV;WWW6C<~oiM=WAO^#=3&30(`wfM3Y zDGlat*cN5sFJf7alCE!2Cp;Bb^(bZ8gxR-A)Aqgaa`JLI(weqQgC=nsLAaT`rmF$< zz~wdHE9b&5VgAZV_N%X|bsae%_Go&BL5%YqPZws=Q)ho0X`8Zfxak-SFui8t&~`x$3e zr9l{R0~U8Dv>bKNnv4H4zhUifLB?%-gn-8hB&3dF$H~Mbmb%L) Sh)*Y#OIX?~KSF$p5APqZAu+@N delta 1559 zcmaJ>O>7%Q6rNr0pV#&}KRC6MlsG?5>b5cEkV~K`p_H}~3J6jOS*EM4XI)l!?d|Nk zk}D%~2vQ_|M6`$Gf`m8)wW6R1dgXuw5|<~ ztQg}6Pp%Rwtq>~T=P?N(#dxSGphZpje&k3^gsKW^>>sMujOP=LSKv|gihHMF+D4X| zS!TM8FMqto`xhcFwY9?pWd92Y)`>}q93@4ba>WQwOo8$=@<8al925~0@q{8hrpQz| zp+rw8>I31qUh0Yj&LtW*WtxDgnTi?BfF+tdj{160B~JTS#G=|9$PfBuVa5MdnDftz zdA}rH@jn+Y`G1Jl+d{>vnt|f5`dy2fED-HFHLF34S_ZT38J<i4A)z9tYy=7s)lEl496+CU+>+6tR$Yf8)u_Q} z!D$AHu3I(B)Af6qMcjPVgw}Gs7opyT-d6e&_;8;Bc!zWcNGh``ydT|7XJJX~riNir zcSojIg&ifmJ3Iw6xtp0-6+Y1R;SsWwkNGc4g*MiUVVM}#F^ISxV^NHRTlIRCor3T) z!qRwP2qD%3WbVfRE2JYlvnP?!>?e`!r{CBax!FmNZoIZ#DDR|cCzaWl+&;Iwle*EF zKD#-$z0f`&WM+}?WWYyVe-?J{v|DK^R5AyJZsyrvZYbt{b&l-3xo!)pa(BTUZ}9h5KTqS~onFLjkk0 zF@(RXHI5YF3{3YUfOkp9|3>{#3eSxD59Q339eae1Lip|r7|kF&<}8mILfA#$K>*GK|Gnh5?a&%-E#;G8RRS5i*Kn-E9?_0i z!7-R&yX?4y;8W0~2K>d#Ov3b7%s}#*j#ayT=qa=qwovFUH1Tg^finLrd`22obAipl s^Bhcf9-tfHIBt*EIN`S};l_R&p;kN^Mx diff --git a/app/schemas/__pycache__/image_standard.cpython-312.pyc b/app/schemas/__pycache__/image_standard.cpython-312.pyc index ffcf4c364a95d8e8b07438a8efdf3fdb648219b6ebaadc29867e0fabd160bf05..17819f7d323f0e09c5c488174333e25abfe9df3f0b1491548a98a50ad40379c3 100644 GIT binary patch literal 4460 zcmc&%O>7&-72YM6zasT-%Zg)rV<%N;%VJu)O=37HWLZx9BiePWCZXLDqumj;?*7m- zyNac#g$@be^rTB09pj_n1_+Q#kW&Gnr(9@g58+;lqUfPLDUgB!IrY8SCAGGx1UXf( zA7|#x-_CsBd*6IjEaoNn``ZU!8J7!^^iQm`|58z5`!gtfEa_5J(q%ni%2uM9kg=RJ zla^9dL|HLYR=S$DGS!Tzr_8LCtLChHH81LEv&Sk_3s$jOl%<69hNNdckn}9gJxqq{ zRC`5h4qEfE);^jL?LE+5c$f(LK8&_j?H9fx_y1)pHHgSccDmoxbTFVz5#2gzw(G_=#D#)|2JM!Ne*&4afxOU z&Y_Z@ZI}+xFBm2*B?1M1!NNI0Eq9J^I)gV7q%IiL)Un4EgS$cYYQr@go0wX3_2Nf~ zD%*dC!p9PosxrVdF$>_VCKD1>^n{-Li_(ToHAT}jqo>Y7Z{bbrnV2^#yxEvH7xU(5 z9>(Tl-kzAZhZbm2mquY`{0}_~*mEgeDh4ALEmEh`i^TK@e#8_rT*?gM+0a9Ltt0M+ zQ8;l9$4ykvR<*E~04Wt?#ia`HY8#)<8ydRcZbT2yCMw(ba*i3A0DJdqotW^B!kQJw znuEJVFzvC*13Bo?DF?4O_CT?TMJwJh7<@i#4mYDymzXBean0SySTFSRUIuf`vt2Jl zqe{3@x3MeP?Fqwp!`pa(EHQTp7^Jx7FzP)IQ{eTBPvYKOd$)q2u|o-ZcAccu8)R!W z+;FWH!~>Hh>KnH(3tM18Ezq^xbrM!LexBBsM4ZFJ3Kk$`%dHaLNWbX&D-G!UkFjf8Uzc9J}oIUs*kW@7Shtr8V;G z`i*A!hCed9dU{-er<=3y`>(A)p9Aa9H;=sX z=$Tg^4V>|n74}SfssAgd(h*<2mswA7YTpx_y6z$Ly<6}}dohYUMvxeKRLBM#V+%%6 zqKY84Cw~x<9sa21o|Cd7o-V?w;=KTNPuj%i2#GTf-KQJ~_je()+@@^fKD*mCe($b)6O4moZCO6Pu73bN5; zOWp)5+M(KBJc|b(4uhjWR_bMgFq|P|*)W=YXgb7V`>{HLMv&<+mIQ&0Ue_wg-heA-5K#m@cig3lZ70Vj%A84 zk7S}eg5GS*+at0}u9IaFdOqeYhz!*uGD`t6OTS>jbqblCyO>G6mtcz#0YDva$Al2% zdiwW+dilg8(-w?Hkx4{IvH?h-Gc?LU(g-ZwZq&!shFvcSUoS_@<_ZcxZ3((py(6 zkT292g@ZH`0iaSUc7-BpS%#^Pp=1c5n8wmEGyV*0;qgu7W^4Gs`cU)uO@H_nz)IUxZnXwP)V}HuT!V;wYE!w^8a=eG zHOp80(V5i?_b&O$Ob9xhzdPvu1D+oZIut%}(1p^b4LbD3L6;M-+6#2~UC;^9RM3kt zNTTq`=)E1l7WBTDw2VNK-NSjvijH2L@J_<)(~ynD(;%itLJEv=PXk8qv2k^bGUl+chzw~M z;(C0!93=*a8Fd5p9g3Q#s1*@QC&56wXAS9u!o)iRTX>RH9fcomIIfhUom~ZC%@Y!M~B1%Eb;0}t|&T2t+9$rOrMEjlh z8<_itb6H!v!r~gFM(&AM8{|^gSS0XRi=?XqZ$Nbkg(|9H5Oh!v25>N3nO^JUz=5uT zN94d4VL!1PSi%94r>5p2gGiwlytk!-J!)$-QvwCSQxv4mj zhBW;lE7XuyYj_lNIgY>d&6G?sLH!fGr7RUs>a zw7b+W?0R@mLP`kXEEIzX*y5lbkL2HF&jNtpE$})!1ry2epA#@!Nm-V^N#tea+x?P! z^k359ze_KOf6whGDf##Ey0IhS?@n)4zALX^+>!8iYgkJ3{5O%3EAke&EAn;{8n#3O P_G~0RDSZn)qto~w$+HCW delta 1480 zcmZuxO>7%Q6rQ#B&%3tM)Nx`rF5Liy8fZi%q(W7wP()iK2qFy}EQ@909VZL>mziBu zmq^YbNT~`^qeW_tfm>CCgpfG&!XZa4IYh`=A;A@igQFxu;(&Owv1x@^+2722-vA$Yzvf3T0_jbcUcHl95*?@A3xK$c*Mj_fK8#Z?<>YD-Se)f$?s zH*^7I&^2VqYsgX_WOx#U8u`?xf-gsu2ibwQ(9!DpcfZ^b;xO~pM7kj`2R8xkA%Yr$ zg&LXoN^ZQ7K}51Lmh_D{OgZq#|M4i6`pDB3VLRERY=}et-_kr$aR!@ zG5#?ZjocR!-6E_-?I5&$FTSE(h;M17$;mmg91#{?g$_**sS+H5833!OUqt2Vy7+mnS218H^vb7T(0b#wb@2=R z0Af&G<4Ha264OjH(}d4V_YocE^Dz$k5O{ys0>B#D z6EE${sAg%S=uNZ^3db`_4tln6>i2a=3a{b&YVv3rar>didAj|O%Mu&RHW)ELwSiAtAoT6;6BT>d>9DTNqE}$k zw~5E_M}#LPl={>lp3%b0=MK15+z3cjor1 zhkWVro_wd-dNC~^QTR)N1N&Hb#ep>)V2WKv|J5aY5;_+4Lc2v}Sf19M$YCe2y+x{W zPb$|`ooPA465l=6ZK7W0Vt2@E^c+O2K;!F)y6n SM*xSlf*^0zj}Q#$`TqfkWqZ^B diff --git a/app/schemas/__pycache__/text_standard.cpython-312.pyc b/app/schemas/__pycache__/text_standard.cpython-312.pyc index f38f664bab416cf000ee7f8a2df9d65cd708df2a97b91755042cad8467c038ba..2a75a684856dc8c5f81eaf44d6aed9104fc5471a131e7c1b9741ce61d143d01b 100644 GIT binary patch delta 87 zcmdllyg``vG%qg~0}vc-Ii0z6BkwvEQy2Y={M=OioW#81#5^-g{o>T3vdrYneErNs m-Q3LNqI`%zs(xZYfqrpvMrv+ivHs@&ETt@rzb2P*dI111fFGLx delta 102 zcmdlWykD62G%qg~0}w2V-#N_P!828K)kJ6-=!qUW? z%#{2R-GWNpl>Fr4n6$*=632qfkks6QoWzpUn8bpDnBwG&)ZE14n9UzpN?90hO-|$V F0svK+CKvz! diff --git a/app/schemas/__pycache__/video_standard.cpython-312.pyc b/app/schemas/__pycache__/video_standard.cpython-312.pyc index b814004a920cc90952a28d09a9ba8a27b6f03ee6183d4e86c0120488c1194c8b..47c3f1b189807f110ad9140e3a8327d9095a8f51d55089e9b4c8040c0f21bef3 100644 GIT binary patch delta 87 zcmew+^<0YgG%qg~0}vc-Ii0zCBku)PQ&;_r{M=OioW#81#5^-g{o>T3vdrYneErNs l-Q3LNqI`%zs(xZYfqrpvMrv+ivHoU7HZFF?Uz2rsy#QEt9=HGi delta 102 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 K#gu*>2K_xoKsyfr diff --git a/app/services/audio/__pycache__/transcription_adapters.cpython-312.pyc b/app/services/audio/__pycache__/transcription_adapters.cpython-312.pyc index 999534bc0345513c1128ceacd1c25a2566f40099fd2a3d80f9ae5c987c8962d2..94224178806e166d351c1324bccf304181163fdd41a67ab2d0de60f7d508c363 100644 GIT binary patch delta 5420 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 K1VA`PtomOx#tjYt delta 2044 zcmZ8iYiv|S6rQ=Sef8dbZy(UIEtKxH6iO+yG*sCFg|;*j-mFW?rS}%M?8~{gR0vQa zm}n%VW_}nGHTo052sXYF6COqp%8wF>n8k{ZKSqt&(h`tp;+%^F!X*36cV2VP%$zg( z_U>O+DC4s1Mr_PzpQL8WUsuXT&M@f>q@x7VvCgG&0^2i};MmNk`HYYdFxZ5&n2{1v z#*uK?GM9EHTud*f-5F29W9y`JNk&e{Hg_h$Mna0xf{r({-<%KlVXTUD zN*Jy=P_=>N_8EqrBT?OmcEzLFT-40vbHvj0Tw9d6y(xB%xVLKQA!7|8IihkzVe670 z1CwJAjo$BH)t2018pKTIwf?=io=1FnrX%Jsm(?v zpVllRspa!Y7Pc?dZeA(k~6*EmUr9;c1)CX&?%{Ep$mncx%X4B*^e(}{=r zWg^vp%}HOC=g2(Rc+(=PizxJCIbT%<#_&U!&xrXe{y*s%Ctana#XBOexasFgm6y#k z3Q_{2i{5R%F30E}N-K{PmCpALmNHurF}Pp!36;vdyI`1*V`C@1doVsW=J%h)oG#MQ zP=r>5BjMB1DM*HmLxKg_!=PuwVdbE3+Hs1d0AsL@t_)V=8oD7IUL+2(%6W2RJqN|X zr=HZZRV8{N7@=PTJ@jleNWTt-=wCsHcgaIf-O1t_={X@P97KaPG#0AFOKE4Q1uvs# z0%3Y|evtKmj_ricGh+??F{IkY7;Ow6wR0+Ecs2Z?x_d5Kj7&VtVYGwQK1QSX`0p?B zJJ2xhXS#mwkaHN#MOG7i%Uy|^=*7ra43BtJdbV_)BEFHsz|CSEo*339R%)+)B(WJ5ETFpgVZx9I89?u#>iSGSpd2Q zfQ1ayT2?CmmR0u60u9s)X#(&7zzLCNfTaM-09pWA0hR-_F(?E#KVJT;a@m2@o`OeD zX(V+(gN`6u=;89p5ql3CAPkZ%_#TCAvNUVREQkj5Dv5)O6977ZK+zW5Rw`qdmX^tz zWFsg2aZpHC5d){!6sFjtQ6Q;>)|2hAv0Ja=E^m zxm}UN-EG{J8Uc7r>~3;gX^@%qN}H=k632Ot>BmJ3T;hSdTs@8ac%Zzeo*%Clfj9C! zOVId|k(%}G{MAT!{Q}`?JqEtOwZ7GHbtwnD1p{vtfw#j#YFWY2*T){tzP<+q1hrFe zz$2>VQx7i459ccY*pF2J*miL9vbS|%e~uUopo)mZUV=XtRY?wTaZ+F7+!6SbAn<=_ z7iq!yFoejUgZK8qBL??(5=}Ei_N^A2D>6BKC~Z7VvP=ihifJ+^@)+YO&WVNT5W>Ej z$Tf+oZ=v}&QQ0J#H;Gy%QO7Mbcnu9sqFF_$s|0Vy6Rqot$R@qdPurwe)r`XvjVp@? d<~6&fVJ>EIC&sRc=G8@HlP-P6CM3f?{2xFa^Y;J% diff --git a/app/services/audio/transcription_adapters.py b/app/services/audio/transcription_adapters.py index 1455a5d..873103b 100644 --- a/app/services/audio/transcription_adapters.py +++ b/app/services/audio/transcription_adapters.py @@ -10,9 +10,14 @@ Propósito: import tempfile import os +import json +from dotenv import load_dotenv from fastapi import HTTPException from openai import OpenAI, AsyncOpenAI import assemblyai as aai +from deepgram import ( + DeepgramClient, +) from app.core.config import settings from app.schemas.audio_standard import AudioRequestFile from app.schemas.audio_standard import StandardTranscriptionResult @@ -24,6 +29,7 @@ async def transcribe_audio_with_provider(audio_request: AudioRequestFile) -> Sta """ Función de adaptador para transcribir audio usando el proveedor de IA configurado. """ + load_dotenv() provider = audio_request.provider.lower() match provider: @@ -31,6 +37,8 @@ async def transcribe_audio_with_provider(audio_request: AudioRequestFile) -> Sta return await transcribe_with_openai(audio_request) case "assemblyai": return await transcribe_with_assemblyai(audio_request) + case "deepgram": + return await transcribe_with_deepgram(audio_request) case _: raise ValueError(f"Proveedor de IA no soportado: {audio_request.provider}") @@ -39,6 +47,7 @@ async def transcribe_with_openai(audio_request: AudioRequestFile) -> StandardTra """ Función de adaptador para transcribir audio usando OpenAI. """ + client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) audio_content = await audio_request.file.read() @@ -112,7 +121,8 @@ async def transcribe_with_assemblyai(audio_request: AudioRequestFile) -> Standar temp_audio_path = temp_audio.name #Definimos el modelo a usar - config = aai.TranscriptionConfig(language_code="es", speaker_labels=audio_request.diarization, + config = aai.TranscriptionConfig(speech_models = [audio_request.model], + language_code="es", speaker_labels=audio_request.diarization, sentiment_analysis=audio_request.sentiment) transcription_obj = aai.Transcriber(config=config).transcribe(temp_audio_path) @@ -159,5 +169,71 @@ async def transcribe_with_assemblyai(audio_request: AudioRequestFile) -> Standar os.unlink(temp_audio_path) except Exception: pass - # Aquí iría la implementación específica para AssemblyAI, similar a la de OpenAI pero usando su SDK y formato de respuesta - pass \ No newline at end of file + +async def transcribe_with_deepgram(audio_request: AudioRequestFile): + """ + Función de adaptador para transcribir audio usando Deepgram. + """ + + #Inicializamos el cliente de Deepgram + deepgram = DeepgramClient(api_key=settings.DEEPGRAM_API_KEY) + audio_content = await audio_request.file.read() + temp_audio_path = None + + # Validar el audio antes de continuar + validate_audio_request(audio_request, audio_content) + + try: + # Crear archivo temporal para el audio + with tempfile.NamedTemporaryFile( + delete=False, + suffix=os.path.splitext(audio_request.file.filename)[1] + ) as temp_audio: + temp_audio.write(audio_content) + temp_audio_path = temp_audio.name + with open(temp_audio_path, "rb") as audio_file: + response = deepgram.listen.v1.media.transcribe_file( + request=audio_file.read(), + model=audio_request.model, + sentiment=audio_request.sentiment, + utterances=audio_request.diarization, + diarize=audio_request.diarization, + # Deepgram no tiene una opción específica de "timestamps", pero sí devuelve marcas de tiempo por segmento, así que no es necesario un parámetro adicional para eso + smart_format=True, + language='es', + ) + response_json = json.loads(response.json()) + + result = StandardTranscriptionResult( + status="success", + original_filename=audio_request.file.filename, + full_transcript=response_json.get("results", {}).get("channels", [{}])[0].get("alternatives", [{}])[0].get("transcript", ""), + model_used=audio_request.model, + provider_used="Deepgram", + confidence_score=response_json.get("results", {}).get("channels", [{}])[0].get("alternatives", [{}])[0].get("confidence"), + segments=[ + { + "text": sentence.get("text", ""), + "speaker": f"Speaker {paragraph.get('speaker')}" if audio_request.diarization and paragraph.get("speaker") is not None else None, + "start_time": sentence.get("start") if audio_request.timestamps else None, + "end_time": sentence.get("end") if audio_request.timestamps else None, + "sentiment": sentence.get("sentiment") if audio_request.sentiment else None + } + for paragraph in response_json.get("results", {}).get("channels", [{}])[0].get("alternatives", [{}])[0].get("paragraphs", {}).get("paragraphs", []) + for sentence in paragraph.get("sentences", []) + ] if (audio_request.diarization or audio_request.timestamps or audio_request.sentiment) else None + ) + return result + except Exception as e: + # Capturamos cualquier error de OpenAI o de lectura de archivos + raise HTTPException( + status_code=500, + detail=f"Error transcribiendo el audio: {str(e)}" + ) + + finally: + if temp_audio_path and os.path.exists(temp_audio_path): + try: + os.unlink(temp_audio_path) + except Exception: + pass \ No newline at end of file diff --git a/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc b/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc index 2971a1352221941e974daee6e0cdc82530df01a2eedb9d10b0c664aa91fe7171..54677b2853489ea1564f9c810386b1eb69f3446c69fe346521386ead43eee39a 100644 GIT binary patch literal 6684 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 delta 1355 zcma)5O>7%Q6rR~Xuh(ANjosL$#My|FcBxVdRHo4U$f}YO6@?;F1S_}J+B*qLcfIcH zIw4RgRf;$vKpLQzst3dYgpd%CkPxTBfddyqkRojw6~Tq0!8w$knAyY-abl$T=6f^m z&Ah+4dGgQP>JO@_06IRZePw$|0Dfaae+YY|_dX>zfCUU-L5p*s0i%^OIJ)uicq+o>^#hkR8F*5W}MyeG@sRhAO z-V}@j`{V?b2lmNHDrZ+XU0v7UuaG48VnX&&5ZKK{U*|}fe^k+7C^fw1g1tz7;>WlU z5GYKn!q=$pmyZH~a}We#Kh&$>Gq}sI1B-iu2jD~Q2LF~Qf){|rNBY?y_J6eFAc*(% zEa576wJ$^~c~{IQPCg0V9{!wPhPp_;7VCN_c`ej5?YY-WcGE{VKw8st>lkSk(!2nh znr&+4m9|aF(#U`q$}Q|I*%rc~=y=NrlXs;2Y91|_PTLGnZP^Z9shM`o^(^E(9x5-= zeKd`+hjk9KYU30IpQE5@_sug;S1-^<{c6i>T=uGG?ckYqy?Ui>I=1BnW381j%WL@6 z1=9~IEqflhEr-5RHCwGJHCwV9$gkS2U@oF6xezNWIePCw_JB=3hz;w?z_xnuPI2rL zccVDInH}#W$G6n6JH-=U3~dyrHnUTm36P07| zOISG~tPfIlI}R(la9b%cu0x8Ay`oWF0BF&`bvk?VfP5>T9)kZC%;NgxU4v2Lc>?p3 zWH2#JR`SE-mL!o|`Jq(-vxdMDg)sHp`RTdJY^`#Bwsvm%o95vpQTaT_4@#&m_eCNpA~c0??U9IxuMiMLVeH4 zWD4rJFjlM44yn~hvGCv%SY`GsgeaGxV){OE>&}X4hrCH4O&jNU!^4OI`4J#Nm^%@{57QU_23HwYfdBvi diff --git a/app/services/image/__pycache__/prompt_builder.cpython-312.pyc b/app/services/image/__pycache__/prompt_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..96cb7aa48576f49ca20348cbcfe9a17361dee6497e05602f30e0932433b20895 GIT binary patch literal 3484 zcmbVPO>ARV6@JflobZyFij?l$J^i2;_P^G&Db>*eNsF*w^pg*f;Zj z%)Rd=ag>p==$3W)5vsaEP^2Pt$Es4qs+&};TFD!bSaoMKU5!}qo%^0+Ky-tpym#OI zIX~a|&N(+9T)E;o`2FRL50rbwasEyp(_bkcJpK*_Z#u%+bA&4jfxGA8T?`6gajzJb z_DZfJO0p#8kBXvvTKtV8=IwLg)W!HqcI=%NFkbxa;-k`OspV9c(lZG27=De6 zu_O6~jQt`y+T;HxC+*qxXM`U@xFnDLAQiF`fmP$OiUi_abu|HSa{VVr3k}BDP_k)& zPY%3qvYqXXruQID8P^~e8>s#u7_n(yoJhf%mcT4$062A|k^vG<4w;BUuA-dc3hcy3 zGLT?q5X46+>XTG{o2{(ye}V!lD>VjHn#H{Xq^CN>0mDE`WC)RYrl0#h5-O3av#qP3 zuE;gs@{zMW+8{8S(UryfRyPQLs00>NG)xoLLvjk%YVWK!+8Ym89I+8uaOof?PFvAr z_~qfOw%E0d=rs>uwd<|dymbH&*wRFLNL+@9O>I(1cIFi%a}*gmL>40=7Ltf{|2v(! zC@~fpWckA!6zkZDv`%HdkYMAZsxSpDeL!ze8xN&M@zBaPg~Xf8p;?snr4&8xKg`htV3K@~d}(31v%Ohg-(rpJt#5T0^yzffzHjM9Prq8IwaP6J$QVg*5PHXfw@bFo3$mF*6gn@e-N2 zeOWc`=6ocJv)YCrQUAon!PXUY`kbJ*`Z`Wg4F&5?xzMG~X_d#w9S*~0*XvhV0aMt$`m|0>Ea3ko>BEY-kbyHn6pf$ zgtob2`v^fk>pk`k*YXE+pR+|!QDAL$Z*8{$i0zmx zIFE+`%5Yv=5Pu#a({X!bahe#a{-n|B$xO2@H}BtTHrmZQ*+Q%FYIC#hJ#oEidHhru z3mJ4o8V)<8V_dS&IhruKh-N+ha<+x$JH{G6UYHh%>YSdVJ&RdpXCkg=6;O%}j6(mJ zo_hK}xzz!82jg;fqc$Xl?k?=f#@Zkb>Dc=*H$J@@!RgC>=dq?UBUS3 zh(1o-&BDZSM(!3~-d4e#e8nj|bKyKQaolI!$%{9ymd@@@UUrt2&o&>!RHr&Wp6_%- z>~}hv92nmO9JMSyrk1%o?FlxHnl7>9B3Npj{=K7bU_xKRY1Li6So!h9!TbHj=k&62{sp#Ub(R1C literal 0 HcmV?d00001 diff --git a/app/services/image/evaluations_adapters.py b/app/services/image/evaluations_adapters.py index f9f286e..0ddc153 100644 --- a/app/services/image/evaluations_adapters.py +++ b/app/services/image/evaluations_adapters.py @@ -8,14 +8,19 @@ Propósito: """ +import json import tempfile import os from fastapi import HTTPException +from matplotlib import image from openai import OpenAI, AsyncOpenAI import assemblyai as aai +from pyparsing.common import Any from app.core.config import settings -from app.schemas.image_standard import ImageRequestFile, StandardImageAnalysisResult +from app.schemas.image_standard import ImageRequestFile, StandardImageAnalysisResult, ImageEvaluationRubric from app.core import config +from app.utilities.image_utilities import json_to_rubric, encode_image_from_bytes +from app.services.image.prompt_builder import build_image_evaluation_prompt # Función de adaptador principal que infiere el proveedor y llama al adaptador específico async def evaluate_image_with_provider(image_request: ImageRequestFile) -> StandardImageAnalysisResult: @@ -24,25 +29,58 @@ async def evaluate_image_with_provider(image_request: ImageRequestFile) -> Stand """ provider = image_request.provider.lower() + content = await image_request.rubric.read() + rubric_dict = json.loads(content) + rubric = json_to_rubric(rubric_dict) + prompt = build_image_evaluation_prompt(rubric) + match provider: case "openai": - return await evaluate_with_openai(image_request) - case "inserte nombre de otra ia aqui": - return await evaluate_with_ai_model2(image_request) - - # AGREGAR OTROS CASOS PARA DIFERENTES PROVEEDORES DE IA AQUÍ - + return await evaluate_with_openai(image_request, prompt) + case "clarifai": + return await evaluate_with_clarifai(image_request, prompt) + case "claude": + return await evaluate_with_claude(image_request, prompt) case _: raise ValueError(f"Proveedor de IA no soportado: {image_request.provider}") # Función de adaptador para evaluar imágenes usando OpenAI -async def evaluate_with_openai(image_request: ImageRequestFile) -> StandardImageAnalysisResult: +async def evaluate_with_openai(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult: """ Función de adaptador para evaluar imágenes usando OpenAI. (Plantilla para futuras implementaciones) """ + client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + image_bytes = await image_request.file.read() + base64_image = encode_image_from_bytes(image_bytes) + + try: + response = await client.chat.completions.create( + model=image_request.model, + messages=[ + {"role": "user", "content": [ + {"type": "text", "text": prompt}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + } + } + ]} + ], + response_format={"type": "json_object"} + ) + + resultado = json.loads(response.choices[0].message.content) + return StandardImageAnalysisResult(**resultado) + except Exception as e: + # Capturamos cualquier error de OpenAI o de lectura de archivos + raise HTTPException( + status_code=500, + detail=f"Error evaluando la imagen: {str(e)}" + ) # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON OPENAI: # 1. Validar la imagen de entrada (tamaño, formato, etc.) # 2. Configurar el cliente de OpenAI con la clave API @@ -51,18 +89,74 @@ async def evaluate_with_openai(image_request: ImageRequestFile) -> StandardImage # 5. Manejar errores y excepciones adecuadamente raise NotImplementedError("La función evaluate_with_openai aún no está implementada.") -async def evaluate_with_ai_model2(image_request: ImageRequestFile) -> StandardImageAnalysisResult: +async def evaluate_with_clarifai(image_request: ImageRequestFile, rubric: ImageEvaluationRubric, prompt: str) -> StandardImageAnalysisResult: """ - Función de adaptador para transcribir video usando otra AI. + Función de adaptador para evaluar imágenes usando Clarifai con un modelo Multimodal. + """ + try: + # 1. Obtener el token de configuración (PAT) + pat = settings.CLARIFAI_API_KEY + if not pat: + raise ValueError("La clave CLARIFAI_API_KEY no está configurada en el entorno.") + + # 2. Obtener la URL del modelo enviada en la petición + model_url = image_request.model + + # Inicializar el modelo de Clarifai + model = Model(url=model_url, pat=pat) + + # 3. Leer los bytes de la imagen subida + image_bytes = await image_request.file.read() + + if not image_bytes: + raise ValueError("El archivo de imagen recibido está vacío.") + + # 4. Preparar el input multimodal para Clarifai combinando la imagen y el prompt de evaluación + multimodal_input = Inputs.get_multimodal_input( + input_id="image_evaluation", + image_bytes=image_bytes, + raw_text=prompt + ) + + # 5. Llamar a la API de Clarifai para evaluar la imagen + predict_response = model.predict([multimodal_input]) + + # Extraer el texto crudo de la respuesta del modelo + raw_output = predict_response.outputs[0].data.text.raw + + # 6. Limpiar la respuesta y convertirla a JSON + # Los LLMs suelen devolver el JSON envuelto en bloques de markdown (```json ... ```) + clean_json = raw_output.replace("```json", "").replace("```", "").strip() + + # Convertir el string limpio a un diccionario de Python + parsed_data = json.loads(clean_json) + + # 7. Retornar el resultado validado contra tu esquema estándar de Pydantic + return StandardImageAnalysisResult(**parsed_data) + + except json.JSONDecodeError as e: + # Error específico si el modelo alucinó texto extra y no devolvió un JSON válido + raise HTTPException( + status_code=500, + detail=f"El modelo de Clarifai no devolvió un JSON válido. Error: {str(e)} | Respuesta cruda: {raw_output if 'raw_output' in locals() else 'N/A'}" + ) + except Exception as e: + # Captura cualquier otro error (problemas de red, token inválido, URL incorrecta, etc.) + raise HTTPException( + status_code=500, + detail=f"Error interno al evaluar con Clarifai: {str(e)}" + ) + +async def evaluate_with_claude(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult: + """ + Función de adaptador para evaluar imágenes usando Claude. (Plantilla para futuras implementaciones) """ - # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE TRANSCRIPCIÓN CON IA (ELEGIR MODELO): - # 1. Validar el video de entrada (tamaño, formato, etc.) - # 2. Configurar el cliente de OpenAI con la clave API - # 3. Llamar a la API de OpenAI para transcribir el video - # 4. Convertir la respuesta de OpenAI al formato estándar de resumen de texto de Qualidot + # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON CLAUDE: + # 1. Validar la imagen de entrada (tamaño, formato, etc.) + # 2. Configurar el cliente de Claude con la clave API + # 3. Llamar a la API de Claude para evaluar la imagen + # 4. Convertir la respuesta de Claude al formato estándar de evaluación de imágenes de Qualidot # 5. Manejar errores y excepciones adecuadamente - raise NotImplementedError("La función transcribe_with_ai_model2 aún no está implementada.") - -# Otros modelos de IA \ No newline at end of file + raise NotImplementedError("La función evaluate_with_claude aún no está implementada.") diff --git a/app/services/image/prompt_builder.py b/app/services/image/prompt_builder.py new file mode 100644 index 0000000..547785b --- /dev/null +++ b/app/services/image/prompt_builder.py @@ -0,0 +1,57 @@ +""" +Módulo encargado de construir el prompt estandarizado para la evaluación de imágenes, +inyectando la rúbrica, el contexto de especialidad y el esquema JSON esperado. +Este módulo es crucial para asegurar que los evaluadores expertos reciban instrucciones +claras y consistentes, y que sus respuestas se ajusten al formato requerido para su posterior +procesamiento y análisis. +""" + + +import json +from app.schemas.image_standard import StandardImageAnalysisResult, ImageEvaluationRubric + +def build_image_evaluation_prompt(rubric: ImageEvaluationRubric) -> str: + """ + Construye el prompt estandarizado inyectando la rúbrica, + el contexto de especialidad y el esquema JSON esperado. + """ + + # 1. Extraemos el JSON de la rúbrica de forma limpia (ignorando nulos) + rubric_json = rubric.model_dump_json(exclude_none=True, indent=2) + + # 2. Extraemos el esquema dinámico de salida basado en Pydantic + expected_output_schema = json.dumps(StandardImageAnalysisResult.model_json_schema(), indent=2) + + # 3. Obtenemos el path de especialización + # Si por alguna razón viene vacío, le damos un rol genérico por defecto + specialization_path = rubric.category if rubric.category else "General Image Analysis" + + # 4. Ensamblamos el prompt con f-strings + prompt = f""" +# ROLE +You are a highly specialized Expert Evaluator with deep domain expertise in the following area: **{specialization_path}**. Your objective is to perform a highly accurate, objective, and domain-calibrated analysis of the provided input based STRICTLY on your specialization and the provided evaluation rubric. + +# TASK +I will provide you with an input and a JSON object representing an `EvaluationRubric`. +Your task is to analyze the input and score it against every criterion and subcriterion defined in the rubric. You must justify your scores with objective feedback based on evidence. + +# DOMAIN CONTEXT +Specialization Path: {specialization_path} + +# RUBRIC +{rubric_json} + +# EVALUATION RULES +1. **Domain Calibration (CRITICAL):** Calibrate your expectations, strictness, and feedback entirely according to the **{specialization_path}** context. Do not evaluate using generalized standards; apply the specific standards expected at this exact level and category. +2. **Strict Adherence:** Evaluate ONLY the criteria and subcriteria listed in the rubric. Do not invent new metrics. +3. **Scoring:** Assign a numeric `score` to each criterion and subcriterion. The score must reflect how well the input meets the description of that specific metric. +4. **Objective Feedback:** Generate constructive, evidence-grounded `feedback` for the overall evaluation. Mention specific elements or patterns observed in the input that justify the scores within the context of the specialization. +5. **Subcriteria Handling:** If a criterion has `subcriteria`, evaluate each subcriterion individually. The parent criterion's score should be a logical aggregate (e.g., average) of its subcriteria scores. + +# OUTPUT FORMAT +You MUST return your response EXCLUSIVELY as a raw, valid JSON object that strictly adheres to the following JSON Schema definition. Do NOT include markdown blocks (```json), explanations, or any text outside the JSON object. + +# EXPECTED JSON SCHEMA +{expected_output_schema} +""" + return prompt \ No newline at end of file diff --git a/app/services/text/__pycache__/evaluations_adapters.cpython-312.pyc b/app/services/text/__pycache__/evaluations_adapters.cpython-312.pyc index c4b6d914ae05caf6df24ac7d95945bb70a047b8ab6f86ba3bfff1caab2ac4d8c..f2550cf367dbab78b2d8054881b9b6c71eae2e3657ff353cf41209bc54ca5a09 100644 GIT binary patch delta 697 zcmZ8fO=uHA7@f)fB-v!+Pqxv3CL7cw+t_Y};14N65TQ_{C|E(kxMs(8akCp{H*GzM zQg0qCBY4(RgNRk};;oc==_TO75*567@)C##2_Bru#)BQ0_q};D^S=2$W_$m8C9xyR z5};#yb;tS`TT2{XtFlue2-B0FWW(; zGAJ{WVUVSuDdbf6iOxC`Jcu1>gFZ$211Jn4(+$^b+2}kzNtcjL&mup=+Dsf?gBx=_ kz+z|Sc7I5E85!P$>$M(Wu~WU#ACjKBNB%XGaf#af1wCfDWB>pF delta 438 zcmdlga#VotG%qg~0}w2V-L z5}usMtj5R5P{Iz<0s<+_ljWJkCf{ThaNJSENOjR*!lvG^@GU^S!E^I8ANP;74rktHR(AD zFm7OUS8yn&<~y8)2&XBQI|0xB#LfDy_d<0n_JYjGE|fJ{)+ zVAK?z%*r`I3uHN1-AabfKqf;GNaHOIo80`A(wtPgA{8K)5r~0~JHn}H$junexWWGe i1Bkxhk@OkN_#nf}7{|CG{sRMuzTg=B1sutk9PG%qg~0}vc-Ii1<5?0Ep%m?4<} delta 108 zcmdlYa9Du%G%qg~0}w2V- Standard match provider: case "openai": return await evaluate_with_openai(text_request) - # AGREGAR OTROS CASOS PARA DIFERENTES PROVEEDORES DE IA AQUÍ + case "claude": + return await evaluate_with_claude(text_request) case _: raise ValueError(f"Proveedor de IA no soportado: {text_request.provider}") @@ -47,4 +48,18 @@ async def evaluate_with_openai(text_request: TextRequestFile) -> StandardTextAna # 5. Manejar errores y excepciones adecuadamente raise NotImplementedError("La función evaluate_with_openai aún no está implementada.") -# Otros modelos de IA \ No newline at end of file +# Función de adaptador para evaluar texto usando Claude +async def evaluate_with_claude(text_request: TextRequestFile) -> StandardTextAnalysisResult: + """ + Función de adaptador para evaluar texto usando Claude. + (Plantilla para futuras implementaciones) + """ + client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + + # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON CLAUDE: + # 1. Validar el texto de entrada (tamaño, formato, etc.) + # 2. Configurar el cliente de Claude con la clave API + # 3. Llamar a la API de Claude para evaluar el texto + # 4. Convertir la respuesta de Claude al formato estándar de evaluación de texto de Qualidot + # 5. Manejar errores y excepciones adecuadamente + raise NotImplementedError("La función evaluate_with_claude aún no está implementada.") \ No newline at end of file diff --git a/app/services/video/__pycache__/transcription_adapters.cpython-312.pyc b/app/services/video/__pycache__/transcription_adapters.cpython-312.pyc index ca3e8be18667d70ab22649e6f45f06e6db21be7488195361d255eaac8293f067..5426b45acc14d0a41ba3e384c783041b1e0f90ccfa9d703be53647cab6f48004 100644 GIT binary patch delta 637 zcmew>bWND=G%qg~0}vc-Ih{F|dm~>ki|8%3^wiwUyiC7aTqWhHIc2FiiAlwiC$X5+ ziRMEkU!=)Y1hT7$A4n840SN_# zUm^M#`MIh3IY4t0^UN&ui&KlrGLtj&^)nN7b2F2R@*x7L`iTVv0#Mo1V*Rqrl+=9v z$!V-sT%tgOK)5)4@+wwkH_Gh-^-Ft*M*%f@HpMz z<+~#!b3xvANA7jIkc)O97i>c>$c9}O3ctV;{(+N0#Qw(vVW-JrY{^We#giFXY}nkv zp)q+co2i-|&>o*eg|yPV4zco-8HFBmw0#DNUg1dw0*~WuUNnIZ91MK?cZ6gv$lLD7y>1tB(JthIZRiEru**W> z7kI)y@H2=wG5&ZU>^S)uYckW<;>nCGHf;U{sdKqnOO!w7XCetu>FN;L8CP3B>S92NA_*W-%m^eDioiAqfCNE=DTpBnBqq<`m@N$o1c>b*xs?o`fg}T1 z$F0eJoVMQlj1i0%EId9ifEm6SpTUd|Dm;wIj2DcZKQMq99?74 Sta provider = video_request.provider.lower() match provider: - case "nombre de la ia 1 aqui": - return await transcribe_with_ai_model1(video_request) - case "nombre de la ia 2 aqui": - return await transcribe_with_ai_model2(video_request) - - # AGREGAR OTROS CASOS PARA DIFERENTES PROVEEDORES DE IA AQUÍ - + case "gemini": + return await transcribe_with_gemini(video_request) + case "twelvelabs": + return await transcribe_with_twelvelabs(video_request) case _: raise ValueError(f"Proveedor de IA no soportado: {video_request.provider}") -# Función de adaptador para transcribir video usando OpenAI -async def transcribe_with_ai_model1(video_request: VideoRequestFile) -> StandardTranscriptionResult: +# Función de adaptador para transcribir video usando Gemini +async def transcribe_with_gemini(video_request: VideoRequestFile) -> StandardTranscriptionResult: """ - Función de adaptador para transcribir video usando OpenAI. + Función de adaptador para transcribir video usando Gemini. (Plantilla para futuras implementaciones) """ # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE TRANSCRIPCIÓN CON IA (ELEGIR MODELO): # 1. Validar el video de entrada (tamaño, formato, etc.) - # 2. Configurar el cliente de OpenAI con la clave API - # 3. Llamar a la API de OpenAI para transcribir el video - # 4. Convertir la respuesta de OpenAI al formato estándar de resumen de texto de Qualidot + # 2. Configurar el cliente de Gemini con la clave API + # 3. Llamar a la API de Gemini para transcribir el video + # 4. Convertir la respuesta de Gemini al formato estándar de resumen de texto de Qualidot # 5. Manejar errores y excepciones adecuadamente - raise NotImplementedError("La función transcribe_with_ai_model1 aún no está implementada.") + raise NotImplementedError("La función transcribe_with_gemini aún no está implementada.") -async def transcribe_with_ai_model2(video_request: VideoRequestFile) -> StandardTranscriptionResult: +async def transcribe_with_twelvelabs(video_request: VideoRequestFile) -> StandardTranscriptionResult: """ - Función de adaptador para transcribir video usando OpenAI. + Función de adaptador para transcribir video usando TwelveLabs. (Plantilla para futuras implementaciones) """ # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE TRANSCRIPCIÓN CON IA (ELEGIR MODELO): # 1. Validar el video de entrada (tamaño, formato, etc.) - # 2. Configurar el cliente de OpenAI con la clave API - # 3. Llamar a la API de OpenAI para transcribir el video - # 4. Convertir la respuesta de OpenAI al formato estándar de resumen de texto de Qualidot + # 2. Configurar el cliente de TwelveLabs con la clave API + # 3. Llamar a la API de TwelveLabs para transcribir el video + # 4. Convertir la respuesta de TwelveLabs al formato estándar de resumen de texto de Qualidot # 5. Manejar errores y excepciones adecuadamente - raise NotImplementedError("La función transcribe_with_ai_model2 aún no está implementada.") + raise NotImplementedError("La función transcribe_with_twelvelabs aún no está implementada.") # Otros modelos de IA \ No newline at end of file diff --git a/app/tests/id.json b/app/tests/id.json new file mode 100644 index 0000000..ef892ac --- /dev/null +++ b/app/tests/id.json @@ -0,0 +1,161 @@ +{ + "rubric": { + "id": "f5a9b2c1-8d3e-4a7f-b12c-9d8e7f6a5b4c", + "name": "Official ID Verification Rubric", + "description": "Rúbrica para evaluar la calidad, legibilidad y validez visual de fotografías de documentos de identificación oficial (ej. INE, Pasaporte o Licencia).", + "user_id": "112b3fda-e380-4919-9f9d-ff941a3b1938", + "status": true, + "visibility": "private", + "verified": true + }, + "category": { + "id": 10, + "name": "National IDs", + "status": true, + "hierarchy": [ + { + "id": 1, + "name": "Identity Verification", + "level": 0, + "parent_id": null + }, + { + "id": 4, + "name": "Document Analysis", + "level": 1, + "parent_id": 1 + }, + { + "id": 7, + "name": "Official IDs", + "level": 2, + "parent_id": 4 + }, + { + "id": 10, + "name": "National IDs", + "level": 3, + "parent_id": 7 + } + ] + }, + "criteria": [ + { + "id": 100, + "name": "Image Quality", + "description": "Evalúa la calidad general de la captura fotográfica del documento.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": null, + "sub_criteria": [ + { + "id": 101, + "name": "Sharpness & Focus", + "description": "La imagen está perfectamente enfocada, sin desenfoque de movimiento, permitiendo ver los microtextos.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 100 + }, + { + "id": 102, + "name": "Lighting & Glare", + "description": "Iluminación uniforme. No hay reflejos del flash (glare) ni sombras densas que oculten el rostro o información vital.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 100 + }, + { + "id": 103, + "name": "Framing & Completeness", + "description": "Los cuatro bordes del documento son claramente visibles. La identificación no está cortada ni parcialmente fuera de la foto.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 100 + } + ] + }, + { + "id": 200, + "name": "Text Readability", + "description": "Evalúa si los datos impresos en el documento son legibles para su extracción.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": null, + "sub_criteria": [ + { + "id": 201, + "name": "Personal Data", + "description": "El nombre completo, fecha de nacimiento y dirección (si aplica) son 100% legibles.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 200 + }, + { + "id": 202, + "name": "Document Identifiers", + "description": "El número de documento, folio o código MRZ (zona de lectura mecánica) son claramente distinguibles.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 200 + } + ] + }, + { + "id": 300, + "name": "Security & Physical Integrity", + "description": "Verificación visual de la integridad física del plástico o papel.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": null, + "sub_criteria": [ + { + "id": 301, + "name": "Face Photograph Clarity", + "description": "La fotografía del rostro impresa en el documento es clara, tiene buen contraste y no presenta alteraciones.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 300 + }, + { + "id": 302, + "name": "No Tampering Signs", + "description": "El documento no presenta roturas, tachaduras, dobleces extremos o signos visuales de manipulación digital/física.", + "type_criteria": "primary", + "type_value": "numeric", + "value": "10", + "status": true, + "criteria_id": 300 + } + ] + } + ], + "references": [ + { + "id": 1, + "title": "KYC Best Practices for ID Verification", + "url": "https://example.com/kyc-standards", + "reference_type": "internal_policy", + "is_primary": true, + "description": "Estándares internos de Qualidot para el proceso de Conoce a tu Cliente (KYC) y prevención de fraude.", + "status": true + } + ] +} \ No newline at end of file diff --git a/app/utilities/__pycache__/audio_utilities.cpython-312.pyc b/app/utilities/__pycache__/audio_utilities.cpython-312.pyc index fa521a5c9b63e98eeccc9f2449562ccb77e8a590d9c5306abd929bf6959e8e74..557b85983367c02a1b34a44f7685b8cdf9fc0b683928b6e44470ada9bdbc0e00 100644 GIT binary patch delta 90 zcmZ1?JXM(QG%qg~0}vc-Ii2~OeIs8Li@Aq>Mt*LpeokUuabljCrG9a0QCVhkX1;!A pqHb~DkiizwWv6zATc>RKgKgnG(gceCUSr_Po2Hf z(X?wVa67xRv$L}^-^|{Zu~-*^_SYL9|COi#!1z=CpnAZ4ZLOKjzlaB7)GPbLj0~LSXm`pX&r02(pH`;9}sY zHjn@U+*6K1%fc=i_zn%7AmaZ|gSd+ZA;El7b)wS~&FK4G~qi%P1kwQ;DUlL8I7kz^O%+yR_RWrPaX2RJ62$kq}7?bP@(2 zDmf4f*aKpLR6?S{qTy00leaaO3J?`4YsDf|4Z=W@2otDZi1tD~aSYs{9STALR56XJ zXN!MEh}l52cW4(I_`7J2N86hnrc&|S%J)A^W$l8I%4?RRSyQJ|jzO-QfV`BcO%zO> z*gnHZfmx}NYvxVYG@O)I%GCBeSzIE_d*v~&Lb@605hw1GLq`5kos zSwYRg;nm(||MTCXo=ETNxvwUt?vCFXzc<~OoUKn@`t(Rmto`(}*w$dOe&x;8i_NJs zce8i0_m&$|bM>h=KRr~%wU;(Wj&BW4)>VD=VoOE?L+jbKZ1vs7bIH}$9-e&hlaYb#&G>=p*xFmo;iJvxo^KA1J&H(&d%i^36AXX2<3j}u+0h?g#P>R*}PUrV{N9a?B?7+;t&y#K<6QGS@-gyWe0IntXNeBOcTL{Q4_d7;f z<0w9|KDRbkJGPN-^dDcDYsN<(#K$({W3`{(ezg&wUYUO+qx~aQYjbjLYhb=E_J1Rz zt6YYw9{KIDPma}JzVweHuitiScw_4K`M)1|eQWTI`Zc>Q4getc4X;nEP1M30qm8{2 zD;K|xD$UsbkJJy;>ebp?jo4&!`qjJd-Fff+a%1{ReOmj?Y&BY&+uT37)qkSCc&*uY z_+j6{TI7$h&te<7d!r5IrAFTjptW0WAxV<|`)w4By}>!Go_{gWJ-atdqkej(mt3e% zoj7EnVc+%z=+=?jUd!#g^}ueoO8cG5waapQv7_(a^Qc*fnOfa z1VvNQnqy3zEK4OfGjS>*P}yhDNP6m|Vd*wDsEiFpAq|pnN=F9J0bfG(ZK z6CgOrgh!xTL0?Dv9z=&WqeIn;jp+D;XlgT>x*cjnPu0a!egbsSkg8%^S5@)?Y@vft z)sl{$H42)O^wm2qJfRxFK8#sfQxP6x*Mkw2{DT>~z0?2XXI7A@1sj+0#tb

&&;# z8R%L9$8q2AJST3;i0k ImageEvaluationRubric: + """Convierte un diccionario JSON en un objeto ImageEvaluationRubric mapeando los campos correctos.""" + + def parse_criteria(criteria_list: list) -> list: + parsed_criteria = [] + for item in criteria_list: + # Convertir el valor a float si existe + score_value = float(item["value"]) if item.get("value") is not None else None + description = str(item["description"]) if item.get("description") is not None else None + + # Procesar subcriterios recursivamente si existen + sub_raw = item.get("sub_criteria") + sub_parsed = parse_criteria(sub_raw) if sub_raw else None + + parsed_criteria.append(ImageEvaluationCriteria( + name=item["name"], + description=description, + score=score_value, + subcriteria=sub_parsed + )) + return parsed_criteria + + # Jerarquía de la categoría + category_info = json_data.get("category", {}) + hierarchy_list = category_info.get("hierarchy", []) + + # Ordenamos por nivel para asegurar la lógica: Nivel 0 -> Nivel 1 -> Nivel 2 + sorted_hierarchy = sorted(hierarchy_list, key=lambda x: x.get("level", 0)) + + # Unimos los nombres con ' > ' + specialization_path = " > ".join([item["name"] for item in sorted_hierarchy]) if sorted_hierarchy else None + + # Extraer la información principal de la rúbrica + rubric_info = json_data.get("rubric", {}) + + return ImageEvaluationRubric( + name=rubric_info.get("name", "Sin nombre"), + description=str(rubric_info["description"]) if rubric_info.get("description") is not None else None, + category=specialization_path, + criteria=parse_criteria(json_data.get("criteria", [])) + ) + +def encode_image_from_bytes(image_bytes: bytes) -> str: + """Codifica imagen desde bytes a base64""" + return base64.b64encode(image_bytes).decode('utf-8') \ No newline at end of file -- 2.49.1 From bc513ad2c4eda0cfc4c5dc0cdb0b9829fad47f3291c17c4e098712942f7aa45d Mon Sep 17 00:00:00 2001 From: lansan69 Date: Tue, 31 Mar 2026 03:54:46 -0600 Subject: [PATCH 2/2] =?UTF-8?q?Im=C3=A1genes=20OpenAI=20y=20Claude?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rubricated_analysis.cpython-312.pyc | Bin 2138 -> 2137 bytes .../v1/endpoints/image/rubricated_analysis.py | 2 +- app/core/__pycache__/config.cpython-312.pyc | Bin 852 -> 934 bytes app/core/config.py | 1 + .../image_standard.cpython-312.pyc | Bin 4460 -> 4894 bytes app/schemas/image_standard.py | 10 ++- .../evaluations_adapters.cpython-312.pyc | Bin 6684 -> 8266 bytes .../prompt_builder.cpython-312.pyc | Bin 3484 -> 3478 bytes app/services/image/evaluations_adapters.py | 77 ++++++++++++++---- app/services/image/prompt_builder.py | 4 +- requirements.txt | 3 +- 11 files changed, 75 insertions(+), 22 deletions(-) diff --git a/app/api/v1/endpoints/image/__pycache__/rubricated_analysis.cpython-312.pyc b/app/api/v1/endpoints/image/__pycache__/rubricated_analysis.cpython-312.pyc index f811b386fc133901ab5304f63d98c24509d67d512136d370d5eaabd8c08b4f91..67e4025f4e07e3b08210e3bb79ed154bb1e0b87bd92f44127102617461cab811 100644 GIT binary patch delta 69 zcmca5a8rQyG%qg~0}$vckq!Y?pN YUuKM$tj{6G6Uq3INr2I&NDZhF03y;9_5c6? diff --git a/app/api/v1/endpoints/image/rubricated_analysis.py b/app/api/v1/endpoints/image/rubricated_analysis.py index 7a0086e..8fe1d8c 100644 --- a/app/api/v1/endpoints/image/rubricated_analysis.py +++ b/app/api/v1/endpoints/image/rubricated_analysis.py @@ -21,7 +21,7 @@ from app.services.image.evaluations_adapters import evaluate_image_with_provider # Inicializar el router de FastAPI para este módulo image_router_analysis = APIRouter() -@image_router_analysis.post("/evaluations/", response_model=StandardImageAnalysisResult) +@image_router_analysis.post("/evaluations", response_model=StandardImageAnalysisResult) async def evaluate_image(image_request: ImageRequestFile = Depends()) -> StandardImageAnalysisResult: """ Endpoint para analizar imágenes usando una rúbrica de evaluación infiriendo el proveedor de IA diff --git a/app/core/__pycache__/config.cpython-312.pyc b/app/core/__pycache__/config.cpython-312.pyc index 8bd36b5dd63c72ac3eafcbf8ff1b24f97602d02f3e1ee906bd0bb25c36b83a0f..4a5b6fe05e4c6894fb30cd2ad7f3d52c5ad31267ed81fd609af0a10995c21784 100644 GIT binary patch delta 187 zcmcb@wv3(kG%qg~0}%LioX$*~$UBMg)x-ryC$5(iWJ_UM%> zc1zIFFT^9rKfu#D-Z8*4-rF_OPm^cz0mdk%BJRoJOreZ5lXICeR4=fI+>lV5!8D(9 zCg%ku(+x$JC9E#8h!%+ejVodW5}K?<{F9$CDM$%}M8LwLK-Mn~o80`A(wtPgB8kZg Y%yMGFjMg6+K!g?pk4U#wBL`S5002rbn*aa+ delta 154 zcmZ3+eua(qG%qg~0}vc-Ii0CCk#`c~k%u|3zG$Yev6Fc?R44!C%hJ;X>L?Ne5#k_1 z3q)vx2pte103swngg%HcoII8P+~huPF~*|F2An37Z*mJUa!!83EgjC4%ALw12~?BH zp2BGZq$`;;xo(LDmn7z;Bo?K3<|d}6I_4$jR2F9z7ij?H{fcx!Ch~#^uq}EZ7T6X8 z5X%rqX!1_><(Kk9I0EcMuo|$7L28OPK?Dzo09juo1tO$DgfWaT2QrF%fJ7_9O_rp| zLIS?*AUQux;mK(NRgA`y?+D0pSpr2EK??IHGYiH^eqrEXEMWQ~#sVZ~cz$I7(O|Oy DDYR1s delta 155 zcmbQI_C|^CG%qg~0}vc-Ii2avGm%e%al=IQjVzV?ngSa?UFBvh-F$+lo{_P1vIn0B zketXD4J6<4P0-T-YAO-~5#k_16GUi%2yGA{03swngbs+%om?v@JNX3vMrJ=vp~7&-6`t8$lDo@)F8?Kwlqg%KWXr1SAIO$}WJ!*H6kGm*>+oQL=B^b=yCgGQ zS&l%Nh8wgF5EL~Qg&RbL4YY@*c7Xuup>-~iBLhVO1Y%rN#MUi*=pnsPl@3l?6rCA~ zvSFZyK5)PJ-kUc&vor6#;oR;oj<~*bIz@ox>Ce8*e-pdo3Ns#fXIop9Ni#B(8I?`5 zwB^zqZdui$S<_YsFy~a8#;1A8Evlf2X;HJM?Nnz}N7{*ftg1_Mr`^0lTPb&_ZCWfH!`!JzvP5d6_|Pg0TADq?}85)X9}9a zU_pNy6-)Wt{MW3TZTI56LLm&MEmth)NZSPw{x^^f$Zq35d?=g1w%0`VRKSGqH6vDK?;KaL{tEsCo(KED z2MhR_NCLg1>DdWTY}TfeRZx+AvZQwz_r(zO8^0IhFlfNgAtPwt4?i}}+C$>UjLGTm zmK4-&Ap6enc03C0z(DM%s4A$PShgE~u=kIew(?Xir&M8ecm`{hee$TNA#INqN}VsJj;GUJ;dik&cE6~smlkaoRj@Gigj?OLCa zaP2wP4Ijaw0q#@xa5r4A5AwqRJeJ_lA?~q%unQ}nA7F6L=Z7HXi$ECeVi$$Ta0k2C zVIjPW#l0vp|B>qn3ww>M?-Cp}9{G~+fbloqBs^%Gly<-)#&xMJlptS2cEhy1Nwy0e zC5EW+P}-m9CT138r%fADN@_Nzn3l4R@}=g^S^e?;lf=oY7IRs(j80%Bv!wzL9hQzCa=yu1*8wgyU|H98RB9AEz=nsqq z{(HuG&L5TG1AM$h9x_cf%iv(1B&24ZhTxOm8q`7dnd|5rHo$;nWmxi9KnOl^(EnM4Y#T()@oCep~x4<;~Ejw+2sqoZ@@u_9NI2>|RP4D383&+o$ z89OnQ85}#2IW_VYBGa5saWi6G%Z@8MCrZlrjIhp2g-tr1zuNSeR<4w0dM@Nk#e}RR zsAnNjC??9qQW5D{xtPFhVzcP(Pav{(!~synL474#R`%{rv4~VCTuu}@pUu$1br~B{ z%(~rLE>58wE@ot84}qN+@&LaC52dLkYT4@4S$m1) z0D%Dl2de~+U`Pd!2R9~DESol^a4nCD1(O|D^o>j**7X5v790>O5jZbDgpZP)0mm|iE^=!A(f#pj@~AH zG@Xrq^Wn_B8z0_S>a`6%jUWA!bI3U9 zOje(JW6R!T&6`~I_SL+7H%~sdd6yz*YPK`aT;ZjbW1r1@rY=R!KXqML;x9C~E{oW( zf=C=g`?lwu+w0xC>z#coHZB=mvDgB^h6ltte|@p$-`$YJR^N)n>2x(%w_m)?H#&en zdiU_1!^={tCZ+1W__A+HwdUJW54F?-Z4Ddncdr7Dx;j88z8u_I3vRvtPA!;P4))f9 zy|?Ypg3-GZcP8$)JPq!+ZU4Kw`~SHXqOV*FZ$Nx6e7klLR)Hh%PrzBkXReld7_X!6 zchV~b5gcZbpHCXKP@A5vpv$qCqBOF{1;Bc5*5cgsJm^m64j)KJy zgO$aoFap`dWMp`NT^z7r{sgkvp6WDhnT%Y_WiqtJXW}*hXr2 z`4NT&3n5%#c*w2#0d)KYc<|5AB3=D|1^7@UcnL1;J=Oq}oVvP7Nn?8ox|VtmH2`k^ j$kJwMY;u*Tjf=)tk)HS-C{hR95(lD=O|Fp^ksIZ|8F+y5 delta 1273 zcmZ8gO>7%Q7@gT&d)NPtca!+1wwENd-bSRfN@TT46QU$kh)^nV39AIl@=mCgSv$Dym>DM$Vx)O~-}}D# zJo9Gu-cujV#_z{sVZg@SEBEU^=dQ)m(g0j5_A7xiW{&45HPObpsU)d-~p_; zAv@%T?XZX?9I>OUCWvEh+>Q%P#R)fQCk5B=h?}xgu5RnDVH-jV;Zb+Y9uqu_$KAA@ zb~AQH#E}i0b#r!(1yPi8Vo2XoocI;RJ|yA<$_p*%B$-wa+6Xe716^}cOfUYgR%e=d zQL>EoJp58>|05^lV3Hl3jM1c;g=spiz7v>YLbiQN9fNR!cC`#lwg1pQhjXbqxD@;p zUWG4!GI;M}HqT4~-(=NdOaW4OL-i*QVkhAw{l{q{G7d-Rkw{V2TOPfdnx}6^PC<)) z8yOE#Wx#dP#E6(&v1loJGWh!d0`NM$8C{1N8jMYC&hT88 zLykdDt(|YyYsl-VF7mwU%lnJ@&Gtg8uhr0wUvD;u%wyT}Nl!o9^q+BeFmh4DN6soC zO=4+8*fBvb#nudd6msYQo@B&J(+9EB%%^PpdgjMO`Lh=pbhUDS8t5k;_O-YXel(b( zf5qpXR^e^9oKe0`tf=t2$db042X}P1oKx-?O9~4*X^CYzImmfFw4%tJ!t^F z0<A?igXp`F*~_hYR=8F*w_1%_z0+=(=+!E2Ree;sQ1{PQYPiZO&1&a%!)!E7 zN47%dQ&Y1|I)7K6SFA^cO)pt5HG7^OFgN9es`);tPdg58;g lKH%gBdHCBsx4kEX{`v>z^knp)h7(>Rgmc>iK?r8;{{?oYF8BZd diff --git a/app/services/image/__pycache__/prompt_builder.cpython-312.pyc b/app/services/image/__pycache__/prompt_builder.cpython-312.pyc index 96cb7aa48576f49ca20348cbcfe9a17361dee6497e05602f30e0932433b20895..e2f87e0ff783cc5af7771926342b1e627cb3d104973a527c67ce73549159b8c0 100644 GIT binary patch delta 99 zcmbOuJx!YLG%qg~0}!xGIi2~HXCvPoMlsRglEl1}#G(|>+{E-$$GpUx%Hqu8&1_8d yTmrV&HT*AX_+Qotybu+AMK)&gRGzm|!5>(37=ql|QLNn&0~Vo{1`Zen_>V_srTWpQS4P-<~$ mPRS;wdM*Ly>l&dKH9{|IgkMNVyds-4xu54PW8~yoUP%ClXdSKq diff --git a/app/services/image/evaluations_adapters.py b/app/services/image/evaluations_adapters.py index 0ddc153..8abdcbf 100644 --- a/app/services/image/evaluations_adapters.py +++ b/app/services/image/evaluations_adapters.py @@ -9,6 +9,8 @@ Propósito: """ import json +import mimetypes +import mimetypes import tempfile import os from fastapi import HTTPException @@ -21,6 +23,7 @@ from app.schemas.image_standard import ImageRequestFile, StandardImageAnalysisRe from app.core import config from app.utilities.image_utilities import json_to_rubric, encode_image_from_bytes from app.services.image.prompt_builder import build_image_evaluation_prompt +from anthropic import Anthropic # Función de adaptador principal que infiere el proveedor y llama al adaptador específico async def evaluate_image_with_provider(image_request: ImageRequestFile) -> StandardImageAnalysisResult: @@ -33,7 +36,7 @@ async def evaluate_image_with_provider(image_request: ImageRequestFile) -> Stand rubric_dict = json.loads(content) rubric = json_to_rubric(rubric_dict) prompt = build_image_evaluation_prompt(rubric) - + match provider: case "openai": return await evaluate_with_openai(image_request, prompt) @@ -74,20 +77,19 @@ async def evaluate_with_openai(image_request: ImageRequestFile, prompt: str) -> ) resultado = json.loads(response.choices[0].message.content) - return StandardImageAnalysisResult(**resultado) + return StandardImageAnalysisResult( + status="success", + original_filename=image_request.file.filename, + provider_used="OpenAI", + model_used=image_request.model, + **resultado + ) except Exception as e: # Capturamos cualquier error de OpenAI o de lectura de archivos raise HTTPException( status_code=500, detail=f"Error evaluando la imagen: {str(e)}" ) - # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON OPENAI: - # 1. Validar la imagen de entrada (tamaño, formato, etc.) - # 2. Configurar el cliente de OpenAI con la clave API - # 3. Llamar a la API de OpenAI para evaluar la imagen - # 4. Convertir la respuesta de OpenAI al formato estándar de evaluación de imágenes de Qualidot - # 5. Manejar errores y excepciones adecuadamente - raise NotImplementedError("La función evaluate_with_openai aún no está implementada.") async def evaluate_with_clarifai(image_request: ImageRequestFile, rubric: ImageEvaluationRubric, prompt: str) -> StandardImageAnalysisResult: """ @@ -150,13 +152,54 @@ async def evaluate_with_clarifai(image_request: ImageRequestFile, rubric: ImageE async def evaluate_with_claude(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult: """ Función de adaptador para evaluar imágenes usando Claude. - (Plantilla para futuras implementaciones) """ + client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + + image_bytes = await image_request.file.read() + base64_image = encode_image_from_bytes(image_bytes) + + media_type = image_request.file.content_type + + if media_type not in ["image/jpeg", "image/png", "image/gif", "image/webp"]: + raise ValueError(f"Tipo de imagen no soportado por Anthropic: {media_type}") + + try: + messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": base64_image + }, + }, + {"type": "text", "text": prompt} + ], + } + ] + + response = client.messages.create( + model=image_request.model, + max_tokens=1024, + messages=messages, + ) + + json_string = response.content[0].text + parsed_data = json.loads(json_string) + + return StandardImageAnalysisResult( + status="success", + original_filename=image_request.file.filename, + provider_used="Claude", + model_used=image_request.model, + **parsed_data + ) - # PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON CLAUDE: - # 1. Validar la imagen de entrada (tamaño, formato, etc.) - # 2. Configurar el cliente de Claude con la clave API - # 3. Llamar a la API de Claude para evaluar la imagen - # 4. Convertir la respuesta de Claude al formato estándar de evaluación de imágenes de Qualidot - # 5. Manejar errores y excepciones adecuadamente - raise NotImplementedError("La función evaluate_with_claude aún no está implementada.") + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error evaluando la imagen: {str(e)}" + ) \ No newline at end of file diff --git a/app/services/image/prompt_builder.py b/app/services/image/prompt_builder.py index 547785b..cddac68 100644 --- a/app/services/image/prompt_builder.py +++ b/app/services/image/prompt_builder.py @@ -8,7 +8,7 @@ procesamiento y análisis. import json -from app.schemas.image_standard import StandardImageAnalysisResult, ImageEvaluationRubric +from app.schemas.image_standard import StandardImageAnalysis, ImageEvaluationRubric def build_image_evaluation_prompt(rubric: ImageEvaluationRubric) -> str: """ @@ -20,7 +20,7 @@ def build_image_evaluation_prompt(rubric: ImageEvaluationRubric) -> str: rubric_json = rubric.model_dump_json(exclude_none=True, indent=2) # 2. Extraemos el esquema dinámico de salida basado en Pydantic - expected_output_schema = json.dumps(StandardImageAnalysisResult.model_json_schema(), indent=2) + expected_output_schema = json.dumps(StandardImageAnalysis.model_json_schema(), indent=2) # 3. Obtenemos el path de especialización # Si por alguna razón viene vacío, le damos un rol genérico por defecto diff --git a/requirements.txt b/requirements.txt index ecc246d..e220608 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ numpy openai langchain langchain-openai -assemblyai \ No newline at end of file +assemblyai +anthropic \ No newline at end of file -- 2.49.1