From c634cb2daa12244f30f182e9feca522f5b60e241 Mon Sep 17 00:00:00 2001 From: iammm0 Date: Wed, 7 Jan 2026 11:56:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0API=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/services/__init__.py | 11 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 315 bytes .../__pycache__/asr_service.cpython-312.pyc | Bin 0 -> 2836 bytes .../__pycache__/pdf_service.cpython-312.pyc | Bin 0 -> 4126 bytes .../__pycache__/tts_service.cpython-312.pyc | Bin 0 -> 2161 bytes api/services/asr_service.py | 64 +++++++++++ api/services/pdf_service.py | 107 ++++++++++++++++++ api/services/tts_service.py | 59 ++++++++++ 8 files changed, 241 insertions(+) create mode 100644 api/services/__init__.py create mode 100644 api/services/__pycache__/__init__.cpython-312.pyc create mode 100644 api/services/__pycache__/asr_service.cpython-312.pyc create mode 100644 api/services/__pycache__/pdf_service.cpython-312.pyc create mode 100644 api/services/__pycache__/tts_service.cpython-312.pyc create mode 100644 api/services/asr_service.py create mode 100644 api/services/pdf_service.py create mode 100644 api/services/tts_service.py diff --git a/api/services/__init__.py b/api/services/__init__.py new file mode 100644 index 0000000..6609a19 --- /dev/null +++ b/api/services/__init__.py @@ -0,0 +1,11 @@ +""" +服务模块 +""" +from .asr_service import asr_service +from .tts_service import tts_service + +__all__ = [ + "asr_service", + "tts_service", +] + diff --git a/api/services/__pycache__/__init__.cpython-312.pyc b/api/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..088e8f91412c32cd74ba3ab92c68ff4e97cbbda3 GIT binary patch literal 315 zcmX@j%ge<81Urt$W-b8Ik3k$5V1hC}YXBM38B!Rc7*ZHhm~t3%8KW2(L2Tw6rd*~d zCLo(7g*BZiin)?alf9G=sGRHBoZhEh3!kl6_;l`cu9u8JAx*|x+=<0S@x`e{WtqvT zAZ|%XF^pTp3{+CY0wnx2S#Gh%$EV~c$HyZ?K|+Z+Iq~r;89oCQG5m6Lwu%V_npGTA zkWpEj852;MoRL_R8&H&=m6}{q9Fvooma3bYoRJ@sSdbY5wX`@UK0Y%qvm`!Vub}c5 zhfQvNN@-52T@fe{KyEBn1QH*Z85tQrGBGi-d}U)`RK3ff@_~(kRp2h20;W zrFf*G^hdLY+nt^LW_EUVc6R^jblMRV<1gP)?^+Q0C+(PpD-ml!AaY1XK_p|DiQ*tG zwoH(LmW{G8F383BARiNg0;U))YKd8cR*V?*O(gSUNEWU#8`^?41}V0yCDlQDnh`Cj zz0kF{^*rxgdh^`k*(>)a-&lF~+Ulj>u1rlVz5b)cYdQNW73daO!+JcatBH6hYOrnb zv|(#ckVHyXa6*J$RP!R@rW88WMyUy1v!~jc$tHr9{~sS zbFwhT1%)&(S`2DZk3vqVVI}nlh;O#=%oYppPyRTJOLNhNI*)~atskl!(#vC!k|Tg! zF4Uc-gR(e-iAy)_Mj1T8UM)#ykVJJBOBy!h^L>%)HmZ-G=FcGV5tP({b+DJ5hFlQ$8RZFUS{cMMg~S; z0bF>U=BLiA%xkraLpP5$gGsUyx zU7mb>`SMH4Z;WjyT>9CKrE}NIVrAm_;tAWLaKjQXVCl_?GBWi77`yVp#ia}HF6FN* zy?P0BZwB6cW3bS)@7&5p7TtNn3^t4f}lTcg>6Sa6{LFNq;B}AdRGOQDt`V1}^ z(gzJe8CEqN07Qu;PAP`1b2zLNg$!2HiD)PE7!x;@s4DGZT30mUr5vjsOG-uaoF*j| z)!H|p#UXpemW`RQqS3403>&YP)KFz*T%R&hw<%$p;(ZFWLJPo+w1WQqA}YAicK2(i z&z`amE=GdBmoV(|1>gQ_wGd2DjNB`73UE`mv>H71x z=eO_7RgYI+I6b@l;Jo+ocbr$8=bh_3ay^F&R;M$tP*;B;kay44HQYEb@B8XIyRPh- z^BtP;9h&pC&G_2pe9zAKo}J$-&hL2)h6@(7uM4jsZr_gc&#kj)#}fqy+PN<`Iz9>} z)OhA=>vB(xKb1Q&e&ljsw)V++_n!05WGpv31t7cd)b5&EBcTl-|aJ>>W2x>3SAuc+S90Ok-4%47()3u}DQJ z?U1DJr9#oth((g*L|Bpty(36~R!va-29a7y(-0w6v!d~%`Jfs@cRBGhj^`gKR6ouK z@;wCv&5fS-o8Alli;^4CR8XuUEmWRHKcQ*f5cG6XjSmod%Mg0?68iQr1bUJ}YVny; zoDw3x^0FXbr@*pJ53EmbEa(XN1`N>WlJ*2t1r}rcDZ^k6VgTF!hMfOE2WQa1zoYtf W;Sdgdif9$ZXj}cO(U+=<3jYh=UDj{_ literal 0 HcmV?d00001 diff --git a/api/services/__pycache__/pdf_service.cpython-312.pyc b/api/services/__pycache__/pdf_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b14da56ad8d0b85b5f0c14695ed1853546d45c4 GIT binary patch literal 4126 zcmcH+ZEO?S@$GuO_S$QQ#EG2^m)@=a9qU?twt9!pA?~k*ZgIb>7#Z%{PPj^DcFX z));NHV7J<&1)>29=G11bCE8-a9yO?iq9F_Rs;$vh8tp<7e+)^!X}Th1RG^V8OjkvY zwq?B$f2Ijg&%WNB;d>vPyPH3G_rrH*-#+hoK!_rg$@D3@5ur_1l?_A2Cf&0Eus}jK z21G5b$^%9gx)E76O#eO+iwP{IN32Q0tR&+j5yosBPzY^rD!yL^Ks02Ns}Y;*Kw6B$ z)-aWjw_{m62&|s8G_1)6R^mEf?Ed{bQ%M6L|7%Glo|5DpdwQ!guI*Vv*7tnBKVH3; zHPq~~3eZU;qbLQop(QHnkmzG5%192`DKRoT%t+2-Ow^TSBQBHZVL--*l(?K(0*p_p zB~I0y;RM(L-@_bqlZZn`ZFRJ&@H%Q_5PGXg)O3~79*NO%C`NX(S{8L6!);?L5hm_I z*HauoS#%g3aU4L0sfeS0j33xPkV+=j_9=-G!{m0$>LJ-s;$p;!!DW%jV>tnqlrhm7 zZm_1}BT7=%<=8MxncVk|#O1W1q>{vNx~vZCM9A=?-W}b8ujw+@2h$^2T^Zb$jgN>} z+lN!5a@^1dRb^OSE5}DtgJN141VW-cAeM$>RirDOHQlk8l2nXXY%DOZ<1TwL04M32 zp?eFJ85g%^jzzxE4ZheHxfA|&zV|b} zz3c?!k|d5aAn4kC4q}>-xE;*5sF#iIG~Rhs{o7UAw~RW;TPqwRG_6_f(D)yMf954ZU-+{TyljV@nEYx{{#9dBMl zSMxS_<_=G@z#===dr$N8yyy5&{ue7A{|9g}qbnBPILF!Ap-|2-+Od#AYI%&Jb0LMT zwE^^z0}@Oya~Wtn;cBZ?3SkK3wW%7=gA;mp4I>Ogsno&l@a)-N&;IEbvwu7_d-;?5 zzdipLg&n86!}q4A?!SF>Ha~g)e4$2C1Absd@h7$?IePY^NnpEs>dM`BKdwq$CsT9<;)>9=t$0lI$W*lTC|QhIHsANpXO1dUR5r z^bC{ghZ9vLk<@Ag{h}tDzRK3sJs*&I>H*0`5eBb>0TcCNQikBw(3yONyg!9CQ8n4h zSWj!Z)QWf(eFAu(-Px8Q3F6+_k3ofrU!EcDuiM7xuJ9P`@l+>1*@j8s5eZ-ts4>Yw zm?Q^GSTRXcI0&7|is`hRlrTv|rqdug!CtaTN)Ak9H#s?((PT_YHCG%|L9Xo7)2c!S zD9h5Ob10*z5-AnrgC+xIn-uY5qEC|{7RO5o$PQ(Ohe2@;DHS(aofM0@>9tObS(xb? zsa&>-@<~fCRSb?w;jBq1AVejXLt$uFnYzgkSogz0RmZFjn8ag~PsmB@1~I5b#zJ*v zNBsgZz8(a%-A7-&h30~&NqAq#vp4&&GQ=^vi% zIjvvVaOOy{W%Z0ZVox7<|3@?K74_Ul3hmG2yxG<-nTZY{j{X-D_fzM`<_I={EVB_59W= zYm35nXZRj_Iygm5t-oNL-&7RV%^bo_;K5dSWpxC1q3s^Rd`ZGoo5uam8Zb^syKw zNez=K5}pkCRl&UgUqla~N0z5z5;ClgJX<{s-7)lcrtM3HbFL`|_fXFEE6rs@#;cuk z)`(xVSt3jEZh%eJ$fhBrRSpVaIi13WDh_q2%21ab1$9gcSSuu$&5x5*p5}a@FawXT zXP|i3t@vOz&99JuxokS4S3(U5CzUv2g^vs5!HGdb=^k2x6sPlsK1E%Z=uz<(3)H5yZBfrk1OM literal 0 HcmV?d00001 diff --git a/api/services/__pycache__/tts_service.cpython-312.pyc b/api/services/__pycache__/tts_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..781f521d6aeaf4ba6778a833e89572fd2a26b1cf GIT binary patch literal 2161 zcmZ`4ZERCj^uF)58)fXHrK`9Of$W8iF++w~%#v*o95Z8K_=9HT+4bGASGu;|_qx%o zHK2=Dbde>>63{Ra&B#D8`hyt!1Br?8&zhkf*O`#WKKMmpBog$8_qKiAT=b?r=iGDd z`FQ8!-k&#YumQ5_embT#ivavVH|FB=#L8hpOaK*lfy$_Cknu8E%z9aZxga;ddwGV+ z_@FQ#dPN4X-~dpCQJ{*`Y|e^TVgZz<^CrEP7%NNB=gHP~ zp4W7U^qxq6OxJq4V*Y*~8t6jd0Q4JrPf+WFbz2x@94Q(G(8NYm)xucRBaloM8BYX#21K-# zuuUVB(n~KoEDRyC!xl2jgT7!CVzD^>K%wp^QlYSU6v~UOu5u}9- z%;^S_Er<>uq5)wGf_fwz(jhL&24H&e_eVn~ur0?^eTEMg=jAzX4Ad<|->e3rZHR4p zEM)XUT^okO6>DaGl}-Ch|B--Y@bnbOl!KD;4@b_7+$*cd@a0y=J*RtOWPBv;tVua* z($4ynvp(%?OgS6p99y!#%8Udy?_B}>W_$9LCwAc8dO=Qh%(<%5uG*BV_E*=gv%*}7 zJ6%$pDyg2_{!2+sh69!L84IY~o_5uxTy?Xq`sBfRcXismGv(enb#T_bH|=gtxtnhc z&AJaHU!5zfnklY&I=^xAW58J*_lq~pl<&B#Pc?i#IN3i_T7Rc__k3B!Um1xA|JLit zr@Uovb1>tr&Q=-Rs$mFyOSZO^akpM{w%NJcR)KJC+Xce8UB(f*le{nFY7F@XprT-# zqL7}61}R;vC~ri4!MsFL6gBKu6jVZdpluZFB;X1_bf~ilN91J&SwL?TeG37j;4Uve z<^`cDW8W$`Cc86$psDUB6y-Fjl59mZbVMIFM3phCg>%g&hRG}H)3UEb);2C$bFrh{ zRHG2VGt0}l@SD*dBA~C8UQ0lRV;JTU%QF0m2pIcs!1_CIECAP^VlCr%1Soi&0UKTK K2TvDet^EVUF(5_& literal 0 HcmV?d00001 diff --git a/api/services/asr_service.py b/api/services/asr_service.py new file mode 100644 index 0000000..9ea2ed3 --- /dev/null +++ b/api/services/asr_service.py @@ -0,0 +1,64 @@ +""" +ASR 服务:语音转文字 +""" +import os +import base64 +from typing import Optional, Any, Coroutine +from openai import OpenAI + + +class ASRService: + """ASR 服务(语音转文字)""" + + def __init__(self): + api_key = os.getenv("OPENAI_API_KEY", "") + if api_key: + self.client = OpenAI(api_key=api_key) + else: + self.client = None + + async def transcribe(self, audio_base64: str) -> str | None: + """ + 转写音频为文字 + + Args: + audio_base64: Base64 编码的音频数据 + + Returns: + 转写文本 + """ + if not self.client: + # 如果没有配置 API Key,返回模拟数据 + return "这是模拟的转写文本(请配置 OPENAI_API_KEY 以使用实际 ASR 功能)" + + try: + # 解码 Base64 音频 + audio_bytes = base64.b64decode(audio_base64) + + # 保存临时文件 + import tempfile + with tempfile.NamedTemporaryFile(suffix=".m4a", delete=False) as tmp_file: + tmp_file.write(audio_bytes) + tmp_file_path = tmp_file.name + + try: + # 调用 OpenAI Whisper API + with open(tmp_file_path, "rb") as audio_file: + transcript = self.client.audio.transcriptions.create( + model="whisper-1", + file=audio_file, + language="zh" # 中文 + ) + return transcript.text + finally: + # 清理临时文件 + import os + if os.path.exists(tmp_file_path): + os.remove(tmp_file_path) + except Exception as e: + # 出错时返回错误信息 + return f"转写失败: {str(e)}" + + +# 全局实例 +asr_service = ASRService() diff --git a/api/services/pdf_service.py b/api/services/pdf_service.py new file mode 100644 index 0000000..0f43167 --- /dev/null +++ b/api/services/pdf_service.py @@ -0,0 +1,107 @@ +""" +PDF 生成服务 +""" +from typing import List +from reportlab.lib.pagesizes import letter, A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfbase.cidfonts import UnicodeCIDFont +from io import BytesIO +import os + + +class PDFService: + """PDF 生成服务""" + + def __init__(self): + # 尝试注册中文字体 + try: + # 使用系统字体或 ReportLab 内置的中文字体 + # 如果没有中文字体文件,使用 UnicodeCIDFont + pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light')) + self.chinese_font = 'STSong-Light' + except Exception: + # 如果注册失败,使用默认字体(可能不支持中文) + self.chinese_font = 'Helvetica' + + async def generate_pdf(self, book, chapters: List) -> bytes: + """ + 生成 PDF + + Args: + book: 回忆录对象 + chapters: 章节列表 + + Returns: + PDF 字节数据 + """ + buffer = BytesIO() + doc = SimpleDocTemplate(buffer, pagesize=A4) + + # 创建样式 + styles = getSampleStyleSheet() + title_style = ParagraphStyle( + 'CustomTitle', + parent=styles['Heading1'], + fontSize=24, + spaceAfter=30, + alignment=1, # 居中 + fontName=self.chinese_font + ) + + heading_style = ParagraphStyle( + 'CustomHeading', + parent=styles['Heading1'], + fontSize=18, + spaceAfter=12, + fontName=self.chinese_font + ) + + normal_style = ParagraphStyle( + 'CustomNormal', + parent=styles['Normal'], + fontSize=12, + leading=18, + fontName=self.chinese_font + ) + + # 构建内容 + story = [] + + # 封面 + story.append(Paragraph(book.title, title_style)) + story.append(Spacer(1, 0.5*inch)) + story.append(PageBreak()) + + # 目录 + story.append(Paragraph("目录", heading_style)) + story.append(Spacer(1, 0.2*inch)) + for i, chapter in enumerate(chapters, 1): + story.append(Paragraph(f"{i}. {chapter.title}", normal_style)) + story.append(PageBreak()) + + # 章节内容 + for chapter in chapters: + story.append(Paragraph(chapter.title, heading_style)) + story.append(Spacer(1, 0.2*inch)) + + # 分段处理内容 + paragraphs = chapter.content.split('\n\n') + for para in paragraphs: + if para.strip(): + story.append(Paragraph(para.strip(), normal_style)) + story.append(Spacer(1, 0.1*inch)) + + story.append(PageBreak()) + + # 生成 PDF + doc.build(story) + buffer.seek(0) + return buffer.read() + + +# 全局实例 +pdf_service = PDFService() diff --git a/api/services/tts_service.py b/api/services/tts_service.py new file mode 100644 index 0000000..4eb158b --- /dev/null +++ b/api/services/tts_service.py @@ -0,0 +1,59 @@ +""" +TTS 服务:文字转语音 +""" +import base64 +import os +from io import BytesIO + +from openai import OpenAI + + +class TTSService: + """TTS 服务(文字转语音)""" + + def __init__(self): + api_key = os.getenv("OPENAI_API_KEY", "") + if api_key: + self.client = OpenAI(api_key=api_key) + else: + self.client = None + + async def synthesize(self, text: str) -> str: + """ + 将文字转换为语音 + + Args: + text: 要转换的文字 + + Returns: + Base64 编码的音频数据 + """ + if not self.client: + # 如果没有配置 API Key,返回空字符串 + return "" + + try: + # 调用 OpenAI TTS API + response = self.client.audio.speech.create( + model="tts-1", + voice="alloy", # 可选: alloy, echo, fable, onyx, nova, shimmer + input=text + ) + + # 读取音频数据 + audio_bytes = BytesIO() + for chunk in response.iter_bytes(): + audio_bytes.write(chunk) + + # 转换为 Base64 + audio_data = audio_bytes.getvalue() + audio_base64 = base64.b64encode(audio_data).decode('utf-8') + return audio_base64 + except Exception as e: + # 出错时返回空字符串 + print(f"TTS 生成失败: {str(e)}") + return "" + + +# 全局实例 +tts_service = TTSService() \ No newline at end of file