From b8140ed820080eef004dfb5d30eb7b3b6d2d71cf Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Thu, 17 Jul 2025 16:15:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86poster=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/__pycache__/main.cpython-312.pyc | Bin 3384 -> 3233 bytes api/main.py | 3 +- api/models/__pycache__/poster.cpython-312.pyc | Bin 5075 -> 5744 bytes api/models/poster.py | 151 ++++---- .../__pycache__/poster.cpython-312.pyc | Bin 4772 -> 3811 bytes api/routers/poster.py | 89 ++--- api/routers/poster_unified.py | 314 ---------------- .../database_service.cpython-312.pyc | Bin 26190 -> 35873 bytes .../__pycache__/poster.cpython-312.pyc | Bin 5368 -> 12700 bytes .../prompt_service.cpython-312.pyc | Bin 31409 -> 31358 bytes api/services/database_service.py | 259 ++++++++++++- api/services/poster.py | 341 ++++++++++++++---- api/services/prompt_service.py | 2 +- 13 files changed, 649 insertions(+), 510 deletions(-) delete mode 100644 api/routers/poster_unified.py diff --git a/api/__pycache__/main.cpython-312.pyc b/api/__pycache__/main.cpython-312.pyc index a1541403d7cb68357927cbfc813e3e6e96bbe321..834e7c3d16f9e450ab31dff484181a08d93275a3 100644 GIT binary patch delta 618 zcmdlXwNR4pG%qg~0}yOqUy<>HVP;CQ(MS$!bj4%@7BwOJP#4$7a4nmKmyRK)e*$6!~<xNzE(Se4N>l zMS%SlYe{)(YRN6Og8br=)S}569MvX<`UOS#xdkOf(m-8R0{V#snfhgh`YHLzrMWNz zC1(Z_yTx8wmYJMily{4{s5Gw#q_;>7M1b{j*yQG?l;)(`71;vCKmk*HU~&NW295;A W>kRQ18R92%@HkJ-<~hj%(gOftCvHFh delta 772 zcmZ1|xkHNYG%qg~0}$|4lxL)HPUMqd?AWNT%BYYl6(z;UkSdU64w6L$slr(nlQS6A z8OwZZ6v+Uss1kq)rsOA==BDPA6v={w*`dNkazI{@{Nz@S%~EPW=4T*gDB=eaw^)ku z^Gk|=9%FEy9K@N#s5p5m=Y_~31&|zHW?phmX-aB*QGO}NoorB=IXxAk2!x4<_H_+Q3o3c%32tGDH64iQLX& ncGrb9E(&X0Fm$}&8F|4Y>H>q%6^7`^>$ue>f8bur0x}K&9MqVj diff --git a/api/main.py b/api/main.py index b000300..aba7b57 100644 --- a/api/main.py +++ b/api/main.py @@ -56,12 +56,11 @@ app.add_middleware( ) # 导入路由 -from api.routers import tweet, poster, poster_unified, prompt, document, data, integration, content_integration +from api.routers import tweet, poster, prompt, document, data, integration, content_integration # 包含路由 app.include_router(tweet.router, prefix="/api/v1/tweet", tags=["tweet"]) app.include_router(poster.router, prefix="/api/v1/poster", tags=["poster"]) -app.include_router(poster_unified.router, prefix="/api/v2/poster", tags=["poster-unified"]) app.include_router(prompt.router, prefix="/api/v1/prompt", tags=["prompt"]) app.include_router(document.router, prefix="/api/v1/document", tags=["document"]) app.include_router(data.router, prefix="/api/v1", tags=["data"]) diff --git a/api/models/__pycache__/poster.cpython-312.pyc b/api/models/__pycache__/poster.cpython-312.pyc index dfee064013e0e00527faa72e7821e835d6fc1c43..325008fcd2c14b25c91bd2c5aeb4945e5d1baf1c 100644 GIT binary patch literal 5744 zcmcIoTTm2d7VeqpVJ^&Y7eNGxKx9;|qEQpH2pGi67zMJ*W-~QTw>a)xNO#Y!SX*0T zyeGODD=K5cCYah7Ojt2hn+*~TdEBaf8EfVN`ejoUW`Np<)kw-JpZ1*p?-?2dvoAZ! z>F)nO{a?<1&iTI6|Fl}o4E%DwIDWJ&mtlUyhvZMM+&ummZu%IG>0&qyr}b#O+AghE z*QN8aU94B%rPpA)&XeXfbQ!$HE~AFgGOY~9o@O{bZ}?J2$LTVW)--4}s9MdW)d;O7 zRcktFHA8E(s-wzU@8Mi@`&6>Z!($n4`WT++(tusHonXN(otEKQPRr?r*e|tIuS?IT z@rFWp;EkOA6}T!oX(}C)O2?qmF{*UTDjkzb$E?yx2OSHYO_?!0rD@@;Dorb&0qe=2 zy>QZ*a3Wc$HD~r}ZQ1g&*Zrcz3$4757n~A*fPXu{i&CHf_CQZ*>dGgH^P%bCZxUa< zLyyZIFhXzq?3wuOyKODEG;%t}i!Q-Uj4PX6exJnqB!`d& za6ELAE-mriJ%g zUZc(QbmgL^sc!cBj<~yJ1ApA%R7iye6?iv`2_lPw;sm&wI1j+a+HN>3}uc&IZLm@im_RiGri0K%3ftibys6e~f<28V<5yBrPy z{ef;W4cm2YpCo5G98RClFF7&fL{M6=e>4^`g3KQF`#od|p#&SrjHP&oNskCLp!f$6 zXP60g^+ajK05@Wel{N==g^c0c%ho8{ym(`|ZLoY~SFF4>Xb`i3@-ojaitf_d%Y+xoC!$5OFZ>B9v z&S=BP>J&k=`HuJlTVNA3_-Ia@9J`S?IZOi|GBi!TG||%JOVtvm&cx4tT}?BC61ze= zj3pbBp&PKkqoO@@BXRYPoF2ewAzlMMiG=mc&1;FF8;VZf-)H*%CR>lV1yOPYM4odv zsaB#t7!Unvo|eVqT%bg|yE(nue0hc6Olgoe*Ixg95 znG5_}SPfd#$1Cx!3Zj2w5jeg0K$nSP!6%P>nsr#TU5r8tg!UAtgFNpi4JT zqHN2=s`5J>k=9?wR=o*24Po8oEiv{@>bE%O;(i;{{2%<*$eC0~ zgnmnK>xdRTQ1L=@3b%}0x=P!+sJ2C=twuTU>P5y@P>*N%OfGY14)wD5Y(7VsKRY=h z=;Ww$azQ6g(aBBfa7HdqwZ>&IYnr07EUClibNQ-qmaEqVk-41e0TLGg5*MZ-vjE^` zg`B?-ZU7H`BL55&$3M6fAH6~&vifICy%e0=T4=zT1&ZMaz?iEkjMJE&`tbB&;60lf zrtV#v8aOFiDLOc4qn(5(1$G+uvL%J)m>ft16Fg6Lr!YO)O;KIx&YFkpsDg+{Llc%l z*OUZ#VLkN886?t^IYayz(O#X8riX7ru935-28~=dj*N&#sldWbW+;R}*<}DRIR2N@ zQ=zl*Gp8UAB`)2Tjb2`Ia!$!PFGMhb;qUPt5Ob+L$r?C4G6v4rkNm*|{DI}nTsr&% z8rH4lGWzx18ZMK|;*+Y_6XYpz<=d&Np8;Lhx;a@dxg`%T8x*gTb&|7NKrT9iRnF5~ zG(=VHOI39nOY3U4Zm8W_TPtTPKCJLRU>kB)^2A-phoI$_ULdJEm9R;!CRzGKt;&YS zdiazPYqlJ#X=rNN-~QXKT?e|GUTxYBrreJvB(&}XTVK6VwCUt*2}q8|K@LGAKqaRi zb&5$EDrZ8BxOh=?`??*H+soSwLIZ3@*oXqnDr`aVG73x(LL&&ffVpKMA41F!wu8Xe z#KRyI`nG-ig^nLq6pa_}8DI6Q@uJ2@CjARJKUtWv^$*jSiWkS%RF2!WjaN2~uWgKM z-yg9Z0E)LPA1Gc$&cht0tW)!YZS8p3n(;MNj|^;a&LdkP62PJb1h8;vU?e-L!Q~NP zoZA}`LIEj~fF~%tgkm#_S`=jG1SZSyk=TGBBpO0yIVjzTITmGy_4o=3L}2mXASgS` zFA6sgcE$2H13T13+06=a?u`}NDRSB;s_O4FeE(Lgssn(tGTbnr8)zReUf=d0XKj@2 z_=VoOO?RY!l*H-|B11eHmIj&!a|d=_|Ko%F>L_~F*v>~huZNA`(vWK3q zE0K(XprDSspO>*xWONtw%w??SlTiX=slpZDi4^}Scrsx%Bxkp!h+?uxL~5hX&52Jw zn;!mDT`E=wuwR639wskg_9_CdWOg_-r`0KLC^o{ z3eTxafWr|-S-Lt&hwl<;yp?jixjF?LihLnTIaEfk#b%Xr)CZ?nN-jjvk|Ftjq~(ei8znB0W{^PDnk0h*BIlK3ImfD2ba3IW9hRaQHypuhr4 zJOF}5(lgKiK(HawaxhkPNS&7tO_Y=kG)FeJ#Y%PsThDid%0gnO^1@qDcGnX&55o=w z4Lh8~Zp$DwqMRo6_5jS&!jI$_0(#`5h_R=abNVE=OEADIw4B6m38WY#Fh)Y3ta<_N z%f?sehoNo4Tkr;yYmA6Sc&O88G>?or4g18*XsV*j`hPJMvuW8HbGU7mfy-=Vn&vl} m(4kodud_Lo&;t)}nSDcJf(O$q1DDx?6>wkpGXod0tN#M;k$6@B literal 5075 zcmb7H>u(cR79ZPVd;CZO0SZ|X2!wWHR!K+#ls;Xo!OWZ{p9n0?~tqy2)=CyiAD+jHv zyjCx1bwjHsueFG@dZD!_ueG?f*jJ)0gjw9?FW1aNKfV0+7PIe+@%>?=|5W;VhkF7S z;Ir%Owt%AQ>??s5RkytyZq>QBqG})#7J|BchZYS=KCAB9Bq-9G5mAD+*8);dY@w^C zC7XS@ABRdeBQbspxW@V(IKXeSG7>9VMcXCzqLteD?UF-+&F-?6L4)KH?JvSJYv;(b zbAy#jbmn?F(UsThk-U;KJA#|qfO|dQ25)YrqE^mVq!+y%QB+CZE`6j)imEMw*{Dl0 z$4{H>@$oCy%)fq4eN(Ib5qgb-ZASlf^TdGJ+5g+kBgT<7BYx5B{c_@L;!bS8dHDzP zn=dn$J1$ssk0>cEa)3BYceX^rsuWhWWiUoMc>~NdKc6*jo`5kj$NSQQN8!8ljjKl2 zkP+|HJ!&KxXyF54QQF(A)#IxA&Y0ibg1*f80i5^T=jQi4>EvK@BrF;2C(YjLu%L;y z&dgB%M6AO+d_gZ#rBE~|s1hF*LQ=tB$pvdQj6D5UFknU7ZV1G)7SY~g6CI*c^%D4UNz{@x@R&*_k>0)5QL-f4jL~TZ@|}V}Mn9 z2=|&!UN*iRGJ9gUHPGft|7tR(&?o zE|);3JO_&A3#`uZ#LGOdyLmno5w)NMbuZ6Eib+6W-6;jm#w^4!YwMkYjZDhcX;KcU@Kz?`ikX0V0|$bHb*&#^BM(@Q-Tfa13OXB3R1o7i=J_6areplc%rh(BaL7w} zd}9^4Jic*}URREdtfD)w*G+moIUXW(oZ}%6lCfSqg)AiszLg#$BID~$WAJ#Eh<@i_ zOO%?nydtA#b+=3zj1Pz;p7SY&{3i|nG$l2}4WWHXIRHiD*8{qTh8Q0e)IHh)FisEM z0%k(vJnA6~{%8n=zG z>j-fA!l{MpDQ?zhk@QAt;WpZ`ZLDlbLK$B5da7(QAgOVL-TZ*%5JgZ>6yZDte>aDs zJzDjzD8@oyx?`+uadt=CZKV>ts6CMq5NR?Jdl{S&>v62wC z`y3A!&$RUXK!*f^!KiaPH6;+16b02mFevPnXpmavTIiPRplF^wG*J{h3%7!(Afc&O zY#d!tbI)$WptL_4l%Cmxl1yiVVdUw^#Mx(z#~SRwq5*CNS3u)&ab@qk;f5`##am-r z+SPbdN9zc?b*y|@Z`HttRQaZ1c7D8Ngx&OjbzsCnK_d=Fn%UW-(DT1W+^n(oC~p_) zM3>~2JiyLgsy9j8I>oGF1a0aOy?H!d1lGk_o1z@o7mFo%HYIsBb8?J77nEmdF2d)4 z@|>s7rA)X3)WDSb#PG^7vw5a{CJl;=k@@?L%#G9Lv3Tak!SO4dG`x$HR#37bJ}O5- zQB@;$$NS9oYdJgJqbZV{HE1RRE1f))P9BClkQ202j2dSy8f|U4FF|-g9xyt4(?gw^ zOXqYiQL}qhUKCUz(@3t7jT*`kx@I$Z!5BPe9Qz!`1u+X)mSjQIWGQp%Bwk}1HB8t* z*L*ZvZxIra7t!r!ph>-~4L8ICv5ajQ*>jE43}K3`o16L_ZBiT8IqV2OoYas37QCP!;Vn z+P@`QHD>nzlIf3SPG2^AI*dc#;`n(Z<7f^TjcQbRK2+f-#Z8T>yQhbgpMy^M1+=is zD2ipooV*T;=dr*jdjd~vfbPw3D=VR(DQwvXqf7sCyRv$8`AT4mWu`7;_b3?Ele>xQ0Av5uJ)b zK;yL%YfV$d`%qDkB`A^}(~J}s@1&}>18i2rl|)l~Z&xiGx*o7@gaH(ESRAq0S3;Dm z?oeB!0g%)-oCfhgK~6A_oUL=wRuN9hffkv=UZQ--N$?qbB?^LoovgEP?K<}&Jw9)c zw}P<+ZgOnAYqMA^_c*JCeZVo6)&F7||IIu->6mMA$G1!}@R+PFf%*rN3_K=Rcr5P3 cbCV1_Cd=nT-8IR;W3m=H6J@`n4cXU!0X&cddH?_b diff --git a/api/models/poster.py b/api/models/poster.py index 17afb23..7b7cc31 100644 --- a/api/models/poster.py +++ b/api/models/poster.py @@ -2,101 +2,122 @@ # -*- coding: utf-8 -*- """ -海报API模型定义 +海报API模型定义 - 简化版本 +只保留核心功能,重点优化图片ID使用追踪 """ -from typing import List, Dict, Any, Optional, Tuple +from typing import List, Dict, Any, Optional from pydantic import BaseModel, Field -class PosterRequest(BaseModel): +class PosterGenerateRequest(BaseModel): """海报生成请求模型""" - content: Dict[str, Any] = Field(..., description="内容数据,包含标题、正文等") - topic_index: str = Field(..., description="主题索引,用于文件命名") - template_name: Optional[str] = Field(None, description="模板名称,如果为None则根据配置选择") + content_id: Optional[int] = Field(None, description="内容ID") + product_id: Optional[int] = Field(None, description="产品ID") + scenic_spot_id: Optional[int] = Field(None, description="景区ID") + image_ids: Optional[List[int]] = Field(None, description="图像ID列表") + generate_collage: bool = Field(False, description="是否生成拼图") class Config: schema_extra = { "example": { - "content": { - "title": "【北京故宫】避开人潮的秘密路线,90%的人都不知道!", - "content": "故宫,作为中国最著名的文化遗产之一...", - "tag": ["北京旅游", "故宫", "旅游攻略", "避暑胜地"] - }, - "topic_index": "1", - "template_name": "vibrant" + "content_id": 1, + "product_id": 2, + "scenic_spot_id": 3, + "image_ids": [1, 2, 3], + "generate_collage": True } } -class PosterResponse(BaseModel): +class ImageUsageInfo(BaseModel): + """图像使用信息模型 - 重点追踪图片使用情况""" + image_id: int = Field(..., description="图像ID") + usage_count: int = Field(..., description="使用次数") + first_used_at: str = Field(..., description="首次使用时间") + last_used_at: str = Field(..., description="最后使用时间") + usage_context: List[str] = Field(default_factory=list, description="使用场景列表") + + +class PosterGenerateResponse(BaseModel): """海报生成响应模型""" request_id: str = Field(..., description="请求ID") - topic_index: str = Field(..., description="主题索引") - poster_path: str = Field(..., description="生成的海报文件路径") - template_name: str = Field(..., description="使用的模板名称") + poster_base64: str = Field(..., description="海报图像的base64编码") + content_info: Optional[Dict[str, Any]] = Field(None, description="内容信息") + product_info: Optional[Dict[str, Any]] = Field(None, description="产品信息") + scenic_spot_info: Optional[Dict[str, Any]] = Field(None, description="景区信息") + used_image_ids: List[int] = Field(default_factory=list, description="使用的图像ID列表") + image_usage_info: List[ImageUsageInfo] = Field(default_factory=list, description="图像使用详情") + collage_base64: Optional[str] = Field(None, description="拼图的base64编码") + metadata: Dict[str, Any] = Field(default_factory=dict, description="处理元数据") class Config: schema_extra = { "example": { "request_id": "poster-20240715-123456-a1b2c3d4", - "topic_index": "1", - "poster_path": "/result/run_20230715_123456/topic_1/poster_vibrant.png", - "template_name": "vibrant" - } - } - - -class TemplateListResponse(BaseModel): - """模板列表响应模型""" - templates: List[str] = Field(..., description="可用的模板列表") - default_template: str = Field(..., description="默认模板") - - class Config: - schema_extra = { - "example": { - "templates": ["vibrant", "business", "collage"], - "default_template": "vibrant" - } - } - - -class PosterTextRequest(BaseModel): - """海报文案生成请求模型""" - system_prompt: str = Field(..., description="系统提示词") - user_prompt: str = Field(..., description="用户提示词") - context_data: Optional[Dict[str, Any]] = Field(None, description="上下文数据,用于填充提示词中的占位符") - temperature: Optional[float] = Field(0.3, description="生成温度参数") - top_p: Optional[float] = Field(0.4, description="top_p参数") - - class Config: - schema_extra = { - "example": { - "system_prompt": "你是一位专业的旅游海报文案撰写专家...", - "user_prompt": "请为{location}的{attraction}创作一段简短有力的海报文案...", - "context_data": { - "location": "北京", - "attraction": "故宫" + "poster_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "content_info": { + "id": 1, + "title": "【北京故宫】避开人潮的秘密路线", + "content": "故宫,作为中国最著名的文化遗产之一...", + "tag": "北京旅游,故宫,旅游攻略" }, - "temperature": 0.3, - "top_p": 0.4 + "used_image_ids": [1, 2, 3], + "image_usage_info": [ + { + "image_id": 1, + "usage_count": 5, + "first_used_at": "2024-07-15 10:30:00", + "last_used_at": "2024-07-15 10:30:00", + "usage_context": ["poster_generation", "collage_creation"] + } + ], + "collage_base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...", + "metadata": { + "total_images_used": 3, + "has_collage": True, + "processing_time": "2.5s" + } } } -class PosterTextResponse(BaseModel): - """海报文案生成响应模型""" - request_id: str = Field(..., description="请求ID") - text_content: Dict[str, Any] = Field(..., description="生成的文案内容") +class ImageUsageRequest(BaseModel): + """图像使用查询请求模型""" + image_ids: List[int] = Field(..., description="要查询的图像ID列表") class Config: schema_extra = { "example": { - "request_id": "text-20240715-123456-a1b2c3d4", - "text_content": { - "title": "紫禁城的秘密", - "subtitle": "600年历史,等你探索", - "description": "穿越时光,触摸历史,感受帝王的荣耀与辉煌" + "image_ids": [1, 2, 3, 4, 5] + } + } + + +class ImageUsageResponse(BaseModel): + """图像使用情况响应模型""" + request_id: str = Field(..., description="请求ID") + image_usage_info: List[ImageUsageInfo] = Field(..., description="图像使用详情") + summary: Dict[str, Any] = Field(..., description="使用情况汇总") + + class Config: + schema_extra = { + "example": { + "request_id": "usage-20240715-123456-a1b2c3d4", + "image_usage_info": [ + { + "image_id": 1, + "usage_count": 5, + "first_used_at": "2024-07-15 10:30:00", + "last_used_at": "2024-07-15 10:30:00", + "usage_context": ["poster_generation", "collage_creation"] + } + ], + "summary": { + "total_images": 5, + "total_usage_count": 15, + "most_used_image_id": 1, + "least_used_image_id": 5 } } } \ No newline at end of file diff --git a/api/routers/__pycache__/poster.cpython-312.pyc b/api/routers/__pycache__/poster.cpython-312.pyc index 1bd83341b9aa76ac4d30914864e43a134bd8ca82..2924d04a063dfc7a16af6d625e82106be146871d 100644 GIT binary patch literal 3811 zcmbVPeQXrh5r6OYKK4HCvwgO)`8bUEI2+rc#t@l^=rAFeq!a}M&8E%edb@Vly1jGW z?(q>pWIhrMAu&=&fRYHO3e;d16D1KOf>4rwkkU$bvL%xhC`jeb<}Z#IDN$3Yows{? z{wTCkpLDbH=FOX#oj3E_ci+0*4g{m){LbGmaU%40Ix&Vl$4s2X2pvZ};t-E{QxtO; zo=nsvF&rbAIkRNpESRpDqgKhr*(8=@B|B%=WR|Eya&k_Mw?Sk-2;rp+phSv$i!1t;$k+`L;T;5~x3o#hJ-Sh#t@+|FRm z;FJenD3tKt!J+|17izZW<*i}9SSaR8z>1mo9kAphn*SJUr9vrR_Ft{}a@NYHSgYD) z3d~Aug9sd%&o5;Tp9yYi9lta*cI*>>oqy~?clyZtV~2V&9}GIuz31;<`*7^NlbOED z>1+Gbhfk01zk2`5k$VT<9^3cD-76(*dfFey7sGt(v?thYv(|)DK3MD%>vmjMudP__346Vg_uCXitvK) zYC-@Hr8C8HS3D*Qs<%}FFI!|FhHBANk*)@TIywX;6wxA6U6c=tA=ngC)FL{A_k_4Y zk{-0`(KYjPHm~iOp=l+8k|422t|qj~umGj=iP=DoBPCa<2<^tZ(QcDbKbuhiCyGF2 z{GChb-uLz6%zW^6`tV1@1>yiheDENpl#~IHsZqb*|BojcNIb4Iw2|<3AqweM;5}N1 z5LV)(A>1XxDm^y2p-W3{eb+AiCG}}Y){{*t!P-1(wirk^dgwx~StrW-R$p8_>|B#H zubBWbdd=PdS<=oTL4z7Qb~@8@RF5@aQawb_Qyh}wybx7wazc{AWS43qxs&ru_Zl%# ztGU6#b3gnhEQLv@ow)^&nkRJ(4y0CtpL~Lp`kOI8s)6Hl z@gN?chX`YjNpb5OLcBwSNdHeFxgfyoh$-KSLw|XX0NFiv!BhKeZ+_fkx{5H8s#iv^GN`UsKcUPrvy_`ob5j zYwus_N%s!j>ppb<%Ave!7m4$Uh@pD-%Ri;x-TN3-IU>ZwNC*ngP|utkN+0?1G3p|P z2na#eVmS3xdVg>0+H}wH@sIi+8HrKv02k3HY^B>WCx+5T&*onAtt)_=THpbT>X>{- z)iL=Js%LtXsy!cuS~$}~f|gRccVFhci}#M593L8L_KQcM3C@{Rr>umPgbcCpf@fhSd9 ziY%a{nv#W-ETW_Ui0arHjwXa>2#FKb8jW`VqN^5xP6=%gm5~*q+Vfw8b7n#jh4|?j zK%<<9Dk>Ia+N88nNCjOem=O+H3DPC-lm7-}9R1Xdva<^;TkuWqyisp+%G-RiZ0=}T zAXOHaWGbshE0?7zm)$I#Gg`VhRl4}*oa)gzO{qCeStlx5HG!-}jvnj7LR7dYRnX9H z1(IYNe!AuK{eVoCr*5&HUe_Vl(bxOJeVdcMzzAD&%Tw9UeAaRqf4XkOvnt81`uUc# zgeq^rIqhUwZGW)8G3jf#T;1auVV~BvBv)+sYvYYqzg=--Npkt-5l>r^ZTmTEq5J+R zH^Ce2!!161AXpSEL)S|wf4$7!Qp8;M*RSy~Uwd4@e_heo;%2^aTY&$jhyk6~Tn$>q z)FtT0EkPSUbNbBq&>x}ppn~;pc4q$@={G-rG^XBx9sdue4&=}mkS~XUfPA)T$d^ag z917)ewG*zFGY5}zC}bLBrl8Oys+xJM&|=fZB>rhAWHELC23mCtoQ6WIeifp%^|7KL9zVNR-!=A!1A1BwV9@jvehBGtX7epaYCwxDrDQRXjie4}fK^NAy+U`V z9ii5fdVhJTaw8>AP_m2?nrre4N|pop&yJa9i!_7IdaAb_h~6 zo7Sb8)(v}~?Xlh}n4c=B?OO*V$<{&-Xk{IqEbEn*@kKf5TlE*7mi2SFvfetfjZ41t zo8fK0O>TWTxjZ!Dc_qodqLp?SN}J-vJ{+va2Nu?#bCfx657siDHU<|l*B4oUy3h)?t~(+YKJ3!UI#@s6pDolQDHS{1sUx=c`?u|gE4-lv`_aT;(*y$YqYL2;IE7m8*eiOS%xxqJie@jGFdC~Rb{os zEQ_qg6Lw8ngiP$g%Dt66FC_y{-0O0PbEJK+)CG&sKBohoW uy@<0WD$K4nY`hGclIwp*jYOsy@nEQF_uMQ)8O;b~s1Y7NFi~Dh{lAYjU2HMnH#*`bhz}C zub2RzafFw_8h%25A>k%?6XXVSmc98ed22M^26)LOyCk>lkvwvR{7)ZF&vPa z1~(W6%N|mtTq9LYR!{J{kl79tt#Oi1_DMBhMUZOuh((W@!P>25W(-zZBG*cFKVc0m zS)}?hORYcRSA$$9`G3NaPhMj9)mUb&ZI>muG*bocIP-rxk$?KF(5BnYop^WQh4-B2 z(Z`KJUUPu#?qo)lsb;%bPRR)=t$BXg*SG1Gp?*1~#*ztGB=pA8swUhV>sK{Cl-L#I zG|z@)Vj#BT&S)aKL#80Gg>DP&kP|A--IY;O8P%9O3W80^H0-cNrVq#Z<)CN^?v@|Q z$Y9y4&)+SlQ^`bH)@u4>B^8gVGVWWHR_UwyBOi@L zz{`ZHRpT3cEqE7Ebk{YnzOwjkFRu1pw$e&vHA55qMs~1c7J=gX;!+@oh-&04A;a7- z8Mc_2w1ot@Of@Vy_uPrxftU2-$iK2b_w*6!262$5OW;B4kkkW7qtQSh@UN@8XfmmG z_0j0VavXxL!gp+-a#T&yu4pO-vv_#vt`rOLs?;w1BlXuvS`Rrz@0!r856CFF=qB!l zm+KFAAMc)V^<;&fMG%um99b?donwJ{q6H$_q9M^2=jG{#8 zF3m>GbG&rdehXSPh8>8 z@x#{NkykB}Fey$L{&Gaca$`fXO1z5UW@FfDwh`2sw3!;aImtd^eayO(+ewD_`^ir3 z7{8MXIx-rlIUxZ8<)^>|jC2M%Iv{Z|8#+4H1#(aB&AszM{>9_@{qOw!!)J5P?#&&X z$RFN!{`Hr?9(_E2?2Y_O`xcHpRa8}zsaSs`mXPEjRG&Y28dfg6|9bAj7eRgDh4J}Q z&wjHhydhGHLe&;~)JVM}6i{?tJQ8kl>{vgSkRg%?cEWR=5 z)GEx>kLdPk3s_}4vP~#vfaWN6Ma>#d?tspxi87^0O0f@Ud|IVJ8x5f5awP3Y&}+H^ z33{t>a@qz@JX#pE5hqre$LZmzp`wtM?@_x8D(#zS{Kf7e`n z(`^09>H3v({^hg&u4#W)!A92qid!VYdgrwLhB4>CI}SIFD^TQ>%jW8q&(*i!pMS+g zhgj(>5YgfM4jVx2H^c(wKeu}ixSw)o>sF43##d)+yH2)_xzF0yf9`D@=d;({{@2x? zJoM>xpIn)}cFS3BU)J9D?}CVW|49eI&)r;)m)jE(LryZ~Kz_>U=yCB=zSTWe{)|-w z{)~$U`6JFQ^Jvo<>~MM~%pCwc8aI+??9jO*v+82F%<7K;k>Uq>;1TBRfpL^E+UVu~{U zqLK1g7At{bV$lvHD}ihb+O#@`j1NQlM<0mGMN4T~iL1JhT#e)!B$(dxXF!62p4n?q za2=A>K++h`VmfOcG;O+;rg*#i1+ID&h@RL^hq#TKYrl53y?463ccy*gw7q4_k@dCA zSG3G6X_~vD^PZIG~7iMzSbmE4}DR`LciZ#qJ4{IS)c06!HFft_mOLHdXYxjLjW zz%Fkr&=K=T#(o&q?t?=wCg}eO)&T6VATApqEdp0*3KAX<4aO6Yz>h*Pyu=^kpA<#H z3IrgrEM+0VCwVhbw~)2u*&2XD_%)#q28V)*iNVHT5!Orz6p1h_up$vBizyF9Qfsb| zY?7ej^@fdM;gTY;n|ni%SVn}$gv*M=k+DH|8FGC8JeZgf1jY0&xN0hq6iq5A28|1U zJiT!GwfupD3vZq}_ihQvGH^F4Uj|)h?8Nt%F_aooBT`h2>QMRg{K;o96HKtx(K2^r zBDZ&MQGfpAF{tC*{=@Sh?O&LDyXYQP1>UeSl%c2bH7AeePQ9Hwup4ZaRwh%C6z&M{ z7)g~);{ExOuhd*+-=ew8JqB); z0$WOO?QF1jI@nvp#u8dK1ZEpLryDv;lIn&-ozHg`T%_YpKu>Fj^IKwV1qclQg!W#n zB$dqwpABuLUR0=p1^>KQ|37L-7$-o`4nR;>sGUqzaG_RVs?vvi3(BWjIpo_NJuCRB zuGKxw{F!DE_%kbbm<_gTwn#)u_D3R`4eukd#173i5QQsFDn=3iC^lyOa$;3?rC^>S z)KR>FQ0yg|t3OHQRgeVmYmOrL;l&Ij(HO<9qIoiEES_F95Ce>eB`JcQ<^%JqjGq?i zRmOb@0Z^;L8TRv_D90|sZcjR};Qg58f>w!ZCH9+BPV~p*G{v?;@is@XZDa3*UvawN39pE#OZsc5g0pZyK4OQnbx(R)i)h>&(wE**19r#RrgHm z`U^HIw}G3hSzh3g`N~^c5RoZZNo{k1O&09L>RWU$X*IFf_ccD=IJRwk*~iO98)vvp zuuI!jhB494!Tr$VFYw5GRkQpXCRyZ(WiwZ>ps=tGP52-8kM(AQH)U^)j{0Y~2mV&{ zhxc$$HlWEE)a(aM6cz&J=3BFEYqOzyaQk~N*?uYNdX^P UnifiedPosterService: - """获取统一海报服务""" - return UnifiedPosterService(ai_agent) - - -def create_response(success: bool, message: str, data: Any = None, request_id: str = None) -> Dict[str, Any]: - """创建标准响应""" - if request_id is None: - request_id = f"req-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - - return { - "success": success, - "message": message, - "request_id": request_id, - "timestamp": datetime.now(timezone.utc).isoformat(), - "data": data - } - - -@router.get("/templates", response_model=TemplateListResponse, summary="获取所有可用模板") -async def get_templates( - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 获取所有可用的海报模板列表 - - 返回每个模板的详细信息,包括: - - 模板ID和名称 - - 模板描述 - - 模板尺寸 - - 必填字段和可选字段 - """ - try: - templates = service.get_available_templates() - response_data = create_response( - success=True, - message="获取模板列表成功", - data=templates - ) - return TemplateListResponse(**response_data) - - except Exception as e: - logger.error(f"获取模板列表失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取模板列表失败: {str(e)}") - - -@router.get("/templates/{template_id}", response_model=BaseAPIResponse, summary="获取指定模板信息") -async def get_template_info( - template_id: str, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 获取指定模板的详细信息 - - 参数: - - **template_id**: 模板ID - """ - try: - template_info = service.get_template_info(template_id) - if not template_info: - raise HTTPException(status_code=404, detail=f"模板 {template_id} 不存在") - - response_data = create_response( - success=True, - message="获取模板信息成功", - data=template_info.dict() - ) - return BaseAPIResponse(**response_data) - - except HTTPException: - raise - except Exception as e: - logger.error(f"获取模板信息失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取模板信息失败: {str(e)}") - - -@router.post("/content/generate", response_model=ContentGenerationResponse, summary="生成海报内容") -async def generate_content( - request: ContentGenerationRequest, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 根据源数据生成海报内容,不生成实际图片 - - 用于: - 1. 预览生成的内容 - 2. 调试和测试内容生成 - 3. 分步骤生成(先生成内容,再生成图片) - - 参数: - - **template_id**: 模板ID - - **source_data**: 源数据,用于AI生成内容 - - **temperature**: AI生成温度参数 - """ - try: - content = await service.generate_content( - template_id=request.template_id, - source_data=request.source_data, - temperature=request.temperature - ) - - response_data = create_response( - success=True, - message="内容生成成功", - data={ - "template_id": request.template_id, - "content": content, - "metadata": { - "generation_method": "ai_generated", - "temperature": request.temperature - } - } - ) - return ContentGenerationResponse(**response_data) - - except Exception as e: - logger.error(f"生成内容失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成内容失败: {str(e)}") - - -@router.post("/generate", response_model=PosterGenerationResponse, summary="生成海报") -async def generate_poster( - request: PosterGenerationRequest, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 生成海报图片 - - 支持两种模式: - 1. 直接提供内容(content字段) - 2. 提供源数据让AI生成内容(source_data字段) - - 参数: - - **template_id**: 模板ID - - **content**: 直接提供的海报内容(可选) - - **source_data**: 源数据,用于AI生成内容(可选) - - **topic_name**: 主题名称,用于文件命名 - - **image_path**: 指定图片路径(可选) - - **image_dir**: 图片目录(可选) - - **output_dir**: 输出目录(可选) - - **temperature**: AI生成温度参数 - """ - try: - result = await service.generate_poster( - template_id=request.template_id, - content=request.content, - source_data=request.source_data, - topic_name=request.topic_name, - image_path=request.image_path, - image_dir=request.image_dir, - output_dir=request.output_dir, - temperature=request.temperature - ) - - response_data = create_response( - success=True, - message="海报生成成功", - data=result, - request_id=result["request_id"] - ) - return PosterGenerationResponse(**response_data) - - except Exception as e: - logger.error(f"生成海报失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成海报失败: {str(e)}") - - -@router.post("/batch", response_model=BaseAPIResponse, summary="批量生成海报") -async def batch_generate_posters( - template_id: str, - base_path: str, - image_dir: str = None, - source_files: Dict[str, str] = None, - output_base: str = "result/posters", - parallel_count: int = 3, - temperature: float = 0.7, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 批量生成海报 - - 自动扫描指定目录下的topic文件夹,为每个topic生成海报。 - - 参数: - - **template_id**: 模板ID - - **base_path**: 包含多个topic目录的基础路径 - - **image_dir**: 图片目录(可选) - - **source_files**: 源文件配置字典(可选) - - **output_base**: 输出基础目录 - - **parallel_count**: 并发处理数量 - - **temperature**: AI生成温度参数 - """ - try: - result = await service.batch_generate_posters( - template_id=template_id, - base_path=base_path, - image_dir=image_dir, - source_files=source_files or {}, - output_base=output_base, - parallel_count=parallel_count, - temperature=temperature - ) - - response_data = create_response( - success=True, - message=f"批量生成完成,成功: {result['successful_count']}, 失败: {result['failed_count']}", - data=result, - request_id=result["request_id"] - ) - return BaseAPIResponse(**response_data) - - except Exception as e: - logger.error(f"批量生成海报失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"批量生成海报失败: {str(e)}") - - -@router.post("/config/reload", response_model=BaseAPIResponse, summary="重新加载配置") -async def reload_config( - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 重新加载海报配置 - - 用于在不重启服务的情况下更新配置,包括: - - 提示词模板 - - 模板配置 - - 默认参数 - """ - try: - service.reload_config() - response_data = create_response( - success=True, - message="配置重新加载成功" - ) - return BaseAPIResponse(**response_data) - - except Exception as e: - logger.error(f"重新加载配置失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"重新加载配置失败: {str(e)}") - - -@router.get("/health", summary="健康检查") -async def health_check(): - """服务健康检查""" - return create_response( - success=True, - message="统一海报服务运行正常", - data={ - "service": "unified_poster", - "status": "healthy", - "version": "2.0.0" - } - ) - - -@router.get("/config", summary="获取服务配置") -async def get_service_config( - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """获取服务配置信息""" - try: - config_info = { - "default_image_dir": service.config_manager.get_default_config("image_dir"), - "default_output_dir": service.config_manager.get_default_config("output_dir"), - "default_font_dir": service.config_manager.get_default_config("font_dir"), - "default_template": service.config_manager.get_default_config("template"), - "supported_image_formats": ["png", "jpg", "jpeg", "webp"], - "available_templates": len(service.get_available_templates()) - } - - response_data = create_response( - success=True, - message="获取配置成功", - data=config_info - ) - return BaseAPIResponse(**response_data) - - except Exception as e: - logger.error(f"获取配置失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取配置失败: {str(e)}") \ No newline at end of file diff --git a/api/services/__pycache__/database_service.cpython-312.pyc b/api/services/__pycache__/database_service.cpython-312.pyc index 7f6e3a441e3f84ca46b7dd8536dcfb546aa7103c..a77b98d12abc572950188f9a517779325970eed1 100644 GIT binary patch delta 3701 zcmai0eNa- z+Ddn)0=;e3b$3>rcDAiuMR9GYT^qH#gVT=tSn@~4ad)OOfI7Q>q>J6zu`|1S?|Vsz zTWB+r`|dmUoO91P?{|Oa-u((6J1vlYnVFf6!QZ~$9^Cube_8qyT+l>J4B#C&z79x; zvb5ae!W}sjntxS5mC_khDlMVYXb~;$7HFX%`gf;LrsF9)NNOcGDadn_!FfJazN~Hq zTrZ$j(V}I1t%mD`)EYVi>=)#$Hp90f^tOz4C0sA2p5gXe3vEauM_dJiUU5icT0e20 zIJ^sA4|aAM@V<$oof;f>fHUra)O#XtuY1o#KdBXfF+*>Frr#-RM zdT2ilr6ru_5^%<-kGY=xd~E*JOIFfhw=phQ$a|8wH*m91kL{IFDlK>+C%$0_|Ur!!RQC_z^F+nPm-JzV3_Z(U`*x2y8xS4o4I@1(ZM!$ zvU+z|)&-2DBUQ|)Pq>Rs0{ja2+Pos3H>^v{m6!4*3iD0L+PK2e$`$oRKUNrrlPn#m zF#S+%4wM$eUH-k&6rbwW>ha%!&8w=E@;?3QNj? z#8PHBbiJCjHcgd>x9wnecFk<_ux%7e?O`>0!xArWS5=A9J^g)ty3aqcpVSDzZ4E@y z>#xmSoJ2^8@6_~~k6jo?WY_^D9^eqmhM#@i)*?Qg{}fpT*G+=)l$PuuPZ z{a$|$%|qZfi>45$?*5@_j)?KeH49g;-Xq5DEl*vE}e}1=Fd&ezj<*scpZlN z;OhCg&)#`>@{_sYcNdGHO>$r*lQrM6mBot&j~CvKDk6`aCeFx#HU&ou)knz9E%t^M zvTi%+VQ6^nTD`qAX>V}Vlg*Azj#knf$U`S&iP|hWvN01-hq(%eKmpQTx|qoZzefgJ zVY-+pm64&BPf$e-It0d~GA5-suo9tCzx3k2P_h4pik;dYk?TjK;Nr`rz@Qda+JgIn z?NcVU72$2V)x)|dR!@gz-H{x_Nan0eeM&kiW%KJpFNSPv-j=Ye1-#U%0?lM@y12W) zhoTt>HZWGD#yicf4v9tJ&I<+QgZNpD68h!56^0F_T)_D`l&Ah$7=MF?DL~$+z=dFUa~_d4 zCjV+G zZbb4}NMuT*A}q%MvMfdVx{xJQHuZA2Z9BWeJ=503ZuPL99#+#Emh?fU%(2#l;6665DJ**tafD?T8}bJ7E&{ggxF~ zuipdrO8gK;(ffi`07C7#kVrZN@N_RpA}MhaNfQnVeFac0&K@;&La-oLASMyv*dEM< z7y|l8rBVh3-8lGnt%*ooS|8GoUkYlr7{o%3e^UJlfS0K7G|+1<#HC==Y`{g}k~ybA z{7Ty4G{`U9Gl^f)17E@f68yr`$!zc>-;*Ak4bHtb@!*%oV>hW7!BoW=o4fyUE!1I8 z_(0u`tCf{}bY^63=-T`{z@QKCt=ORW?)EyhPlCfkQl#TfeYBGO-Lx5b?=Gbt8UQ7}u3m9Dn zd===c))$#XeBT>KUT&cw&NkS+wAe1dxz>eMV z?FjJy!9f0VO56x8Z76BWRG?c~MD~0{o;#Ac$U3L`NBd{gB||TKt2FISIoc6%G!=_m!#=AX5iMFH&Qete2ZB9QrD0bayKVQ3tAlOnWIKV=vTnT( z5%EZEWqk#9i>Nih$L$KST}Rx0Mr&6QUu5BM{)I{iN0Y$mG~o;)15*jmc3sayU5&Yf zhHGe;M8gdlKv`#hNJUm_9wt$4uM%*A&k1L`W ze0B=U~!8eU~Ikbch;3MSX9&8IN&`x$&2kWNT xT{>!d8$I39{TOL53Y1g^983!44(OyA{7+zpF2Se4ceFjrpNXsgiJ_5G{2xQ!KdJx# delta 164 zcmZ2DgX!EEM!wU$yj%=Ga4NYx<45pDzAQ$jIHt{2jPZ<&8k2dLwb`XxSfj*KKBa$Vwo#(>FnNmXo`On!cv3X?Ck?Vh~8 zU6v`9Y4f@EC??@A42rB0MZ!S4inxJsDvnGWlG$3x6pmqr@i$ I5D8WY0DtE)ga7~l diff --git a/api/services/__pycache__/poster.cpython-312.pyc b/api/services/__pycache__/poster.cpython-312.pyc index fc910e47822cdfb11be1ba7e5e8962fd1943f583..01a4145ca4d005171021c4972978f37b599ba3a5 100644 GIT binary patch literal 12700 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 diff --git a/api/services/__pycache__/prompt_service.cpython-312.pyc b/api/services/__pycache__/prompt_service.cpython-312.pyc index 6b2c7c24c1cbfb1bc8913e87a8c99376a9c594f5..79230af3914922b0a245d558a89c243c1ffe8e0e 100644 GIT binary patch delta 1069 zcmdn^mGR#fM&8rByj%=G5IwIV!)GIJAv2@W=6Ysj#>t0Rnix4JyRsHezQS6@SUK67 zt%|9aXY*CIn~aRolh<>YvdI9o-IAI7mqTXrTMk8LpeR320;A040-h6WY??sHBCW}G z!XdiaAa$H2MTyC&Nr}nXx46>si*gf7;!`VOg~hq!TzXjo722!Kgm@sfv*l*q|yDgNn*PT!YDms@6aQa#c@qgrU0uq(B@< z++xd5%1TWxDQX0XG=T_p5YY@G5`ly!XA#(j9*_XssW2OQ^t2#hDKhzip2+44dfVB< zaazS*P?VoiN`zfgu-bLcOdIGMF7s_%K%XCUY!(8CCfGJmRA>qTV<60Ft}H0@iiLoL z0tP6O*(~6^gcTUwTRk)wWhUSAn93+OIWJ0bvtQ5;=FP3X^B4^f&R+|11|-TrEO2nK z7ME1!q#}pbsK|rEsU2iu0f<2O z@D_+`F!^zbCeUQw(o7~cS&)d~8& delta 1040 zcma))%WD&H5XbW~+0CZyLTv2AJPl21wt3Jf%|TnRx!4NQOCbs#Qk$SzYMX45h6an; zmX<>C;P?QsV8L4ytt%=X1TUiYQV&uLE%Z+?__`HmHbPqKD~B(?-|Wn1X1>2{ePPRA znYbc~bvpdbR`TahEQ=XEi>jCOZH!%n%;6^3JT5Q>ONMDy2VO1%cLXQgv zfVjoBH;93yOM-==V)0^4%RVR1+z_#zG*>IBJU|izuy!MR)oSCz z6zGJC^&pn)x5Z7nljl7?=7YzcLsmN_7fEE2GbH0AVc7Bbjp#j&*Qi-?PTI~~Ha9Vn+`cj#7-)BDPP%u@RW>TgY%;d?BIVDG8d#-`tbufG*jn@m?o(WSf* Optional[Dict[str, Any]]: + """ + 根据ID获取图像信息 + + Args: + image_id: 图像ID + + Returns: + 图像信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM material WHERE id = %s AND materialType = 'image' AND isDelete = 0", + (image_id,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到图像信息: ID={image_id}, 名称={result['materialName']}") + return result + else: + logger.warning(f"未找到图像信息: ID={image_id}") + return None + + except Exception as e: + logger.error(f"查询图像信息失败: {e}") + return None + + def get_images_by_ids(self, image_ids: List[int]) -> List[Dict[str, Any]]: + """ + 根据ID列表批量获取图像信息 + + Args: + image_ids: 图像ID列表 + + Returns: + 图像信息列表 + """ + if not self.db_pool or not image_ids: + return [] + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + + # 构建IN查询 + placeholders = ','.join(['%s'] * len(image_ids)) + query = f"SELECT * FROM material WHERE id IN ({placeholders}) AND materialType = 'image' AND isDelete = 0" + cursor.execute(query, image_ids) + results = cursor.fetchall() + cursor.close() + conn.close() + + logger.info(f"批量查询图像信息: 请求{len(image_ids)}个,找到{len(results)}个") + return results + + except Exception as e: + logger.error(f"批量查询图像信息失败: {e}") + return [] + + def get_content_by_id(self, content_id: int) -> Optional[Dict[str, Any]]: + """ + 根据ID获取内容信息 + + Args: + content_id: 内容ID + + Returns: + 内容信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM content WHERE id = %s AND isDelete = 0", + (content_id,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到内容信息: ID={content_id}, 标题={result['title']}") + return result + else: + logger.warning(f"未找到内容信息: ID={content_id}") + return None + + except Exception as e: + logger.error(f"查询内容信息失败: {e}") + return None + + def get_content_by_topic_index(self, topic_index: str) -> Optional[Dict[str, Any]]: + """ + 根据主题索引获取内容信息 + + Args: + topic_index: 主题索引 + + Returns: + 内容信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM content WHERE topicIndex = %s AND isDelete = 0 ORDER BY createTime DESC LIMIT 1", + (topic_index,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到内容信息: topicIndex={topic_index}, 标题={result['title']}") + return result + else: + logger.warning(f"未找到内容信息: topicIndex={topic_index}") + return None + + except Exception as e: + logger.error(f"查询内容信息失败: {e}") + return None + + def get_images_by_folder_id(self, folder_id: int) -> List[Dict[str, Any]]: + """ + 根据文件夹ID获取图像列表 + + Args: + folder_id: 文件夹ID + + Returns: + 图像信息列表 + """ + if not self.db_pool: + return [] + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM material WHERE folderId = %s AND materialType = 'image' AND isDelete = 0 ORDER BY createTime DESC", + (folder_id,) + ) + results = cursor.fetchall() + cursor.close() + conn.close() + + logger.info(f"根据文件夹ID获取图像: folderId={folder_id}, 找到{len(results)}个图像") + return results + + except Exception as e: + logger.error(f"根据文件夹ID获取图像失败: {e}") + return [] + + def get_folder_by_id(self, folder_id: int) -> Optional[Dict[str, Any]]: + """ + 根据ID获取文件夹信息 + + Args: + folder_id: 文件夹ID + + Returns: + 文件夹信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM material_folder WHERE id = %s AND isDelete = 0", + (folder_id,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到文件夹信息: ID={folder_id}, 名称={result['folderName']}") + return result + else: + logger.warning(f"未找到文件夹信息: ID={folder_id}") + return None + + except Exception as e: + logger.error(f"查询文件夹信息失败: {e}") + return None + + def get_related_images_for_content(self, content_id: int, limit: int = 10) -> List[Dict[str, Any]]: + """ + 获取与内容相关的图像列表 + + Args: + content_id: 内容ID + limit: 限制数量 + + Returns: + 相关图像列表 + """ + if not self.db_pool: + return [] + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + + # 获取内容信息 + cursor.execute( + "SELECT * FROM content WHERE id = %s AND isDelete = 0", + (content_id,) + ) + content = cursor.fetchone() + + if not content: + cursor.close() + conn.close() + return [] + + # 获取相关的图像(这里可以根据业务逻辑调整查询条件) + cursor.execute( + "SELECT * FROM material WHERE materialType = 'image' AND isDelete = 0 ORDER BY RAND() LIMIT %s", + (limit,) + ) + results = cursor.fetchall() + cursor.close() + conn.close() + + logger.info(f"获取相关内容图像: contentId={content_id}, 找到{len(results)}个图像") + return results + + except Exception as e: + logger.error(f"获取相关内容图像失败: {e}") + return [] \ No newline at end of file diff --git a/api/services/poster.py b/api/services/poster.py index 709264d..55fb6fc 100644 --- a/api/services/poster.py +++ b/api/services/poster.py @@ -2,20 +2,22 @@ # -*- coding: utf-8 -*- """ -海报服务层 -封装现有功能,提供API调用 +海报服务层 - 简化版本 +封装核心功能,重点优化图片使用追踪 """ import logging import uuid -from typing import List, Dict, Any, Optional, Tuple +import time +from typing import List, Dict, Any, Optional from datetime import datetime from core.config import ConfigManager, PosterConfig from core.ai import AIAgent from utils.file_io import OutputManager +from utils.image_processor import ImageProcessor from poster.poster_generator import PosterGenerator -from poster.text_generator import PosterContentGenerator +from api.services.database_service import DatabaseService logger = logging.getLogger(__name__) @@ -38,86 +40,291 @@ class PosterService: # 初始化各个组件 self.poster_generator = PosterGenerator(config_manager, output_manager) - self.text_generator = PosterContentGenerator(ai_agent) - def generate_poster(self, content: Dict[str, Any], topic_index: str, - template_name: Optional[str] = None) -> Tuple[str, str, str, str]: + # 初始化数据库服务 + self.db_service = DatabaseService(config_manager) + + # 图片使用追踪存储(实际应用中应该使用数据库) + self._image_usage_tracker = {} + + def generate_poster_simplified(self, content_id: Optional[int] = None, + product_id: Optional[int] = None, + scenic_spot_id: Optional[int] = None, + image_ids: Optional[List[int]] = None, + generate_collage: bool = False) -> Dict[str, Any]: """ - 生成海报 + 简化的海报生成方法 Args: - content: 内容数据,包含标题、正文等 - topic_index: 主题索引,用于文件命名 - template_name: 模板名称,如果为None则根据配置选择 + content_id: 内容ID + product_id: 产品ID + scenic_spot_id: 景区ID + image_ids: 图像ID列表 + generate_collage: 是否生成拼图 Returns: - 请求ID、主题索引、生成的海报文件路径和使用的模板名称 + 包含base64图像数据和图片使用信息的字典 """ - logger.info(f"开始为主题 {topic_index} 生成海报") + start_time = time.time() + logger.info(f"开始生成海报: content_id={content_id}, product_id={product_id}, scenic_spot_id={scenic_spot_id}") - # 生成海报 - poster_path = self.poster_generator.generate_poster(content, topic_index, template_name) + result = { + "request_id": f"poster-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}", + "poster_base64": "", + "content_info": None, + "product_info": None, + "scenic_spot_info": None, + "used_image_ids": [], + "image_usage_info": [], + "collage_base64": None, + "metadata": {} + } - # 获取使用的模板名称 - if template_name is None: - template_name = self.poster_generator._select_template() - - # 生成请求ID - request_id = f"poster-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - - logger.info(f"海报生成完成,请求ID: {request_id}, 主题索引: {topic_index}, 模板: {template_name}") - return request_id, topic_index, poster_path, template_name - - def get_available_templates(self) -> Tuple[List[str], str]: - """ - 获取可用的模板列表 - - Returns: - 可用的模板列表和默认模板 - """ - # 获取配置 - poster_config = self.config_manager.get_config('poster', PosterConfig) - - # 获取可用模板 - available_templates = poster_config.available_templates - - # 获取默认模板 - default_template = poster_config.template_selection - if default_template == "random" and available_templates: - default_template = available_templates[0] + try: + # 1. 获取内容信息 + if content_id: + content_info = self.db_service.get_content_by_id(content_id) + if content_info: + result["content_info"] = self._build_content_info(content_info) - return available_templates, default_template + # 2. 获取产品信息 + if product_id: + product_info = self.db_service.get_product_by_id(product_id) + if product_info: + result["product_info"] = self._build_product_info(product_info) + + # 3. 获取景区信息 + if scenic_spot_id: + scenic_spot_info = self.db_service.get_scenic_spot_by_id(scenic_spot_id) + if scenic_spot_info: + result["scenic_spot_info"] = self._build_scenic_spot_info(scenic_spot_info) + + # 4. 处理图像信息并追踪使用情况 + image_paths = [] + if image_ids: + images = self.db_service.get_images_by_ids(image_ids) + for img in images: + image_paths.append(img['filePath']) + result["used_image_ids"].append(img['id']) + # 更新图片使用追踪 + self._update_image_usage(img['id'], "poster_generation") + + # 5. 构建海报内容 + poster_content = self._build_poster_content_from_info( + result["content_info"], result["product_info"], result["scenic_spot_info"] + ) + + # 6. 生成海报(只使用vibrant模板) + poster_path = self.poster_generator.generate_poster( + poster_content, + str(content_id or product_id or scenic_spot_id or "unknown"), + "vibrant" + ) + + if poster_path: + # 转换为base64 + result["poster_base64"] = ImageProcessor.image_to_base64(poster_path) + + # 7. 处理拼图 + if generate_collage and len(image_paths) > 1: + collage_result = ImageProcessor.process_images_for_poster( + image_paths, + target_size=(900, 1200), + create_collage=True + ) + + if collage_result.get("collage_image"): + result["collage_base64"] = collage_result["collage_image"]["base64"] + # 更新拼图中使用的图片使用追踪 + for img_id in result["used_image_ids"]: + self._update_image_usage(img_id, "collage_creation") + + # 8. 构建图片使用信息 + result["image_usage_info"] = self._get_image_usage_info(result["used_image_ids"]) + + # 9. 添加元数据 + processing_time = time.time() - start_time + result["metadata"] = { + "total_images_used": len(result["used_image_ids"]), + "has_collage": result["collage_base64"] is not None, + "processing_time": f"{processing_time:.2f}s", + "template_used": "vibrant" + } + + logger.info(f"海报生成完成,处理时间: {processing_time:.2f}s") + return result + + except Exception as e: + logger.error(f"生成海报失败: {e}", exc_info=True) + result["metadata"]["error"] = str(e) + return result - async def generate_poster_text(self, system_prompt: str, user_prompt: str, - context_data: Optional[Dict[str, Any]] = None, - temperature: Optional[float] = None, - top_p: Optional[float] = None) -> Tuple[str, Dict[str, Any]]: + def get_image_usage_info(self, image_ids: List[int]) -> Dict[str, Any]: """ - 生成海报文案 + 获取图像使用情况信息 Args: - system_prompt: 系统提示词 - user_prompt: 用户提示词 - context_data: 上下文数据,用于填充提示词中的占位符 - temperature: 生成温度参数 - top_p: top_p参数 + image_ids: 图像ID列表 Returns: - 请求ID和生成的文案内容 + 图像使用情况信息 """ - logger.info("开始生成海报文案") + request_id = f"usage-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - # 生成文案 - text_content = await self.text_generator.generate_text_for_poster( - system_prompt=system_prompt, - user_prompt=user_prompt, - context_data=context_data, - temperature=temperature, - top_p=top_p - ) + image_usage_info = [] + total_usage_count = 0 + usage_counts = [] - # 生成请求ID - request_id = f"text-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" + for img_id in image_ids: + usage_info = self._get_single_image_usage_info(img_id) + if usage_info: + image_usage_info.append(usage_info) + total_usage_count += usage_info["usage_count"] + usage_counts.append(usage_info["usage_count"]) - logger.info(f"海报文案生成完成,请求ID: {request_id}") - return request_id, text_content \ No newline at end of file + # 计算汇总信息 + summary = { + "total_images": len(image_ids), + "total_usage_count": total_usage_count, + "most_used_image_id": None, + "least_used_image_id": None + } + + if usage_counts: + max_usage = max(usage_counts) + min_usage = min(usage_counts) + summary["most_used_image_id"] = next( + (info["image_id"] for info in image_usage_info if info["usage_count"] == max_usage), + None + ) + summary["least_used_image_id"] = next( + (info["image_id"] for info in image_usage_info if info["usage_count"] == min_usage), + None + ) + + return { + "request_id": request_id, + "image_usage_info": image_usage_info, + "summary": summary + } + + def _update_image_usage(self, image_id: int, context: str): + """更新图片使用追踪""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + if image_id not in self._image_usage_tracker: + self._image_usage_tracker[image_id] = { + "usage_count": 0, + "first_used_at": current_time, + "last_used_at": current_time, + "usage_context": [] + } + + tracker = self._image_usage_tracker[image_id] + tracker["usage_count"] += 1 + tracker["last_used_at"] = current_time + if context not in tracker["usage_context"]: + tracker["usage_context"].append(context) + + def _get_single_image_usage_info(self, image_id: int) -> Optional[Dict[str, Any]]: + """获取单个图片的使用信息""" + if image_id in self._image_usage_tracker: + tracker = self._image_usage_tracker[image_id] + return { + "image_id": image_id, + "usage_count": tracker["usage_count"], + "first_used_at": tracker["first_used_at"], + "last_used_at": tracker["last_used_at"], + "usage_context": tracker["usage_context"] + } + else: + # 如果图片从未被使用过,返回默认信息 + return { + "image_id": image_id, + "usage_count": 0, + "first_used_at": "", + "last_used_at": "", + "usage_context": [] + } + + def _get_image_usage_info(self, image_ids: List[int]) -> List[Dict[str, Any]]: + """获取图片使用信息列表""" + usage_info = [] + for img_id in image_ids: + info = self._get_single_image_usage_info(img_id) + if info: + usage_info.append(info) + return usage_info + + def _build_content_info(self, content_data: Dict[str, Any]) -> Dict[str, Any]: + """构建内容信息""" + return { + "id": content_data["id"], + "title": content_data["title"], + "content": content_data["content"], + "tag": content_data["tag"] + } + + def _build_product_info(self, product_data: Dict[str, Any]) -> Dict[str, Any]: + """构建产品信息""" + return { + "id": product_data["id"], + "name": product_data["name"], + "description": product_data.get("description"), + "real_price": float(product_data["realPrice"]) if product_data.get("realPrice") else None, + "origin_price": float(product_data["originPrice"]) if product_data.get("originPrice") else None + } + + def _build_scenic_spot_info(self, scenic_data: Dict[str, Any]) -> Dict[str, Any]: + """构建景区信息""" + return { + "id": scenic_data["id"], + "name": scenic_data["name"], + "description": scenic_data.get("description"), + "address": scenic_data.get("address") + } + + def _build_poster_content_from_info(self, content_info: Optional[Dict[str, Any]], + product_info: Optional[Dict[str, Any]], + scenic_spot_info: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """从信息构建海报内容""" + title = "" + content_parts = [] + tags = [] + + # 构建标题 + if content_info: + title = content_info["title"] + if content_info.get("content"): + content_parts.append(content_info["content"]) + if content_info.get("tag"): + tags.extend(content_info["tag"].split(",")) + else: + # 如果没有内容信息,使用景区和产品信息构建标题 + if scenic_spot_info and product_info: + title = f"{scenic_spot_info['name']} - {product_info['name']}" + elif scenic_spot_info: + title = scenic_spot_info['name'] + elif product_info: + title = product_info['name'] + + # 添加景区信息 + if scenic_spot_info and scenic_spot_info.get("description"): + content_parts.append(f"景区介绍: {scenic_spot_info['description']}") + tags.append(scenic_spot_info["name"]) + + # 添加产品信息 + if product_info: + if product_info.get("description"): + content_parts.append(f"产品介绍: {product_info['description']}") + if product_info.get("real_price"): + content_parts.append(f"价格: ¥{product_info['real_price']}") + tags.append(product_info["name"]) + + content = "\n\n".join(content_parts) if content_parts else "暂无详细内容" + + return { + "title": title, + "content": content, + "tag": tags + } \ No newline at end of file diff --git a/api/services/prompt_service.py b/api/services/prompt_service.py index 5fae265..04b5376 100644 --- a/api/services/prompt_service.py +++ b/api/services/prompt_service.py @@ -51,7 +51,7 @@ class PromptService: self.db_pool = self._init_db_pool() # 创建必要的目录结构 - self._create_resource_directories() + # self._create_resource_directories() def _create_resource_directories(self): pass