main.py 154 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820
  1. import os
  2. import threading
  3. import schedule
  4. import random
  5. import asyncio, aiohttp
  6. import traceback
  7. import copy
  8. import json, re
  9. from functools import partial
  10. import http.cookies
  11. from typing import *
  12. # 按键监听语音聊天板块
  13. import keyboard
  14. import pyaudio
  15. import wave
  16. import numpy as np
  17. import speech_recognition as sr
  18. from aip import AipSpeech
  19. import signal
  20. import time
  21. import http.server
  22. import socketserver
  23. from utils.my_log import logger
  24. from utils.common import Common
  25. from utils.config import Config
  26. from utils.my_handle import My_handle
  27. """
  28. ___ _
  29. |_ _| | ____ _ _ __ ___ ___
  30. | || |/ / _` | '__/ _ \/ __|
  31. | || < (_| | | | (_) \__ \
  32. |___|_|\_\__,_|_| \___/|___/
  33. """
  34. config = None
  35. common = None
  36. my_handle = None
  37. last_liveroom_data = None
  38. last_username_list = None
  39. # 空闲时间计数器
  40. global_idle_time = 0
  41. # 配置文件路径
  42. config_path = "config.json"
  43. # web服务线程
  44. async def web_server_thread(web_server_port):
  45. Handler = http.server.SimpleHTTPRequestHandler
  46. with socketserver.TCPServer(("", web_server_port), Handler) as httpd:
  47. logger.info(f"Web运行在端口:{web_server_port}")
  48. logger.info(
  49. f"可以直接访问Live2D页, http://127.0.0.1:{web_server_port}/Live2D/"
  50. )
  51. httpd.serve_forever()
  52. """
  53. _oo0oo_
  54. o8888888o
  55. 88" . "88
  56. (| -_- |)
  57. 0\ = /0
  58. ___/`---'\___
  59. .' \\| |// '.
  60. / \\||| : |||// \
  61. / _||||| -:- |||||- \
  62. | | \\\ - /// | |
  63. | \_| ''\---/'' |_/ |
  64. \ .-\__ '-' ___/-. /
  65. ___'. .' /--.--\ `. .'___
  66. ."" '< `.___\_<|>_/___.' >' "".
  67. | | : `- \`.;`\ _ /`;.`/ - ` : | |
  68. \ \ `_. \_ __\ /__ _/ .-` / /
  69. =====`-.____`.___ \_____/___.-`___.-'=====
  70. `=---='
  71. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  72. 佛祖保佑 永不宕机 永无BUG
  73. """
  74. # 点火起飞
  75. def start_server():
  76. global \
  77. config, \
  78. common, \
  79. my_handle, \
  80. last_username_list, \
  81. config_path, \
  82. last_liveroom_data
  83. global do_listen_and_comment_thread, stop_do_listen_and_comment_thread_event
  84. global faster_whisper_model, sense_voice_model, is_recording, is_talk_awake, wait_play_audio_num
  85. # 按键监听相关
  86. do_listen_and_comment_thread = None
  87. stop_do_listen_and_comment_thread_event = threading.Event()
  88. # 冷却时间 0.5 秒
  89. cooldown = 0.5
  90. last_pressed = 0
  91. # 正在录音中 标志位
  92. is_recording = False
  93. # 聊天是否唤醒
  94. is_talk_awake = False
  95. # 待播放音频数量(在使用 音频播放器 或者 metahuman-stream等不通过AI Vtuber播放音频的对接项目时,使用此变量记录是是否还有音频没有播放完)
  96. wait_play_audio_num = 0
  97. # 获取 httpx 库的日志记录器
  98. # httpx_logger = logging.getLogger("httpx")
  99. # 设置 httpx 日志记录器的级别为 WARNING
  100. # httpx_logger.setLevel(logging.WARNING)
  101. # 最新的直播间数据
  102. last_liveroom_data = {
  103. "OnlineUserCount": 0,
  104. "TotalUserCount": 0,
  105. "TotalUserCountStr": "0",
  106. "OnlineUserCountStr": "0",
  107. "MsgId": 0,
  108. "User": None,
  109. "Content": "当前直播间人数 0,累计直播间人数 0",
  110. "RoomId": 0,
  111. }
  112. # 最新入场的用户名列表
  113. last_username_list = [""]
  114. my_handle = My_handle(config_path)
  115. if my_handle is None:
  116. logger.error("程序初始化失败!")
  117. os._exit(0)
  118. # Live2D线程
  119. try:
  120. if config.get("live2d", "enable"):
  121. web_server_port = int(config.get("live2d", "port"))
  122. threading.Thread(
  123. target=lambda: asyncio.run(web_server_thread(web_server_port))
  124. ).start()
  125. except Exception as e:
  126. logger.error(traceback.format_exc())
  127. os._exit(0)
  128. if platform != "wxlive":
  129. """
  130. /@@@@@@@@ @@@@@@@@@@@@@@@]. =@@@@@@@
  131. =@@@@@@@@@^ @@@@@@@@@@@@@@@@@@` =@@@@@@@
  132. ,@@@@@@@@@@@` @@@@@@@@@@@@@@@@@@@^ =@@@@@@@
  133. .@@@@@@\@@@@@@. @@@@@@@^ .\@@@@@@\ =@@@@@@@
  134. /@@@@@/ \@@@@@\ @@@@@@@^ =@@@@@@@ =@@@@@@@
  135. =@@@@@@. .@@@@@@^ @@@@@@@\]]]@@@@@@@@^ =@@@@@@@
  136. ,@@@@@@^ =@@@@@@` @@@@@@@@@@@@@@@@@@/ =@@@@@@@
  137. .@@@@@@@@@@@@@@@@@@@. @@@@@@@@@@@@@@@@/` =@@@@@@@
  138. /@@@@@@@@@@@@@@@@@@@\ @@@@@@@^ =@@@@@@@
  139. =@@@@@@@@@@@@@@@@@@@@@^ @@@@@@@^ =@@@@@@@
  140. ,@@@@@@@. ,@@@@@@@` @@@@@@@^ =@@@@@@@
  141. @@@@@@@^ =@@@@@@@. @@@@@@@^ =@@@@@@@
  142. """
  143. # HTTP API线程
  144. def http_api_thread():
  145. import uvicorn
  146. from fastapi import FastAPI
  147. from fastapi.middleware.cors import CORSMiddleware
  148. from utils.models import (
  149. SendMessage,
  150. LLMMessage,
  151. CallbackMessage,
  152. CommonResult,
  153. )
  154. # 定义FastAPI应用
  155. app = FastAPI()
  156. # 允许跨域
  157. app.add_middleware(
  158. CORSMiddleware,
  159. allow_origins=["*"],
  160. allow_credentials=True,
  161. allow_methods=["*"],
  162. allow_headers=["*"],
  163. )
  164. # 定义POST请求路径和处理函数
  165. @app.post("/send")
  166. async def send(msg: SendMessage):
  167. global my_handle, config
  168. try:
  169. tmp_json = msg.dict()
  170. logger.info(f"内部HTTP API send接口收到数据:{tmp_json}")
  171. data_json = tmp_json["data"]
  172. if "type" not in data_json:
  173. data_json["type"] = tmp_json["type"]
  174. if data_json["type"] in ["reread", "reread_top_priority"]:
  175. my_handle.reread_handle(data_json, type=data_json["type"])
  176. elif data_json["type"] == "comment":
  177. my_handle.process_data(data_json, "comment")
  178. elif data_json["type"] == "tuning":
  179. my_handle.tuning_handle(data_json)
  180. elif data_json["type"] == "gift":
  181. my_handle.gift_handle(data_json)
  182. elif data_json["type"] == "entrance":
  183. my_handle.entrance_handle(data_json)
  184. return CommonResult(code=200, message="成功")
  185. except Exception as e:
  186. logger.error(f"发送数据失败!{e}")
  187. return CommonResult(code=-1, message=f"发送数据失败!{e}")
  188. @app.post("/llm")
  189. async def llm(msg: LLMMessage):
  190. global my_handle, config
  191. try:
  192. data_json = msg.dict()
  193. logger.info(f"API收到数据:{data_json}")
  194. resp_content = my_handle.llm_handle(
  195. data_json["type"], data_json, webui_show=False
  196. )
  197. return CommonResult(
  198. code=200, message="成功", data={"content": resp_content}
  199. )
  200. except Exception as e:
  201. logger.error(f"调用LLM失败!{e}")
  202. return CommonResult(code=-1, message=f"调用LLM失败!{e}")
  203. @app.post("/callback")
  204. async def callback(msg: CallbackMessage):
  205. global my_handle, config, global_idle_time, wait_play_audio_num
  206. try:
  207. data_json = msg.dict()
  208. # 特殊回调特殊处理
  209. if data_json["type"] == "audio_playback_completed":
  210. wait_play_audio_num = int(data_json["data"]["wait_play_audio_num"])
  211. wait_synthesis_msg_num = int(data_json["data"]["wait_synthesis_msg_num"])
  212. logger.info(f"内部HTTP API callback接口 音频播放完成回调,待播放音频数量:{wait_play_audio_num},待合成消息数量:{wait_synthesis_msg_num}")
  213. else:
  214. logger.info(f"内部HTTP API callback接口收到数据:{data_json}")
  215. # 音频播放完成
  216. if data_json["type"] in ["audio_playback_completed"]:
  217. wait_play_audio_num = int(data_json["data"]["wait_play_audio_num"])
  218. # 如果等待播放的音频数量大于10
  219. if data_json["data"]["wait_play_audio_num"] > int(
  220. config.get(
  221. "idle_time_task", "wait_play_audio_num_threshold"
  222. )
  223. ):
  224. logger.info(
  225. f'等待播放的音频数量大于限定值,闲时任务的闲时计时由 {global_idle_time} -> {int(config.get("idle_time_task", "idle_time_reduce_to"))}秒'
  226. )
  227. # 闲时任务的闲时计时 清零
  228. global_idle_time = int(
  229. config.get("idle_time_task", "idle_time_reduce_to")
  230. )
  231. return CommonResult(code=200, message="callback处理成功!")
  232. except Exception as e:
  233. logger.error(f"callback处理失败!{e}")
  234. return CommonResult(code=-1, message=f"callback处理失败!{e}")
  235. logger.info("HTTP API线程已启动!")
  236. uvicorn.run(app, host="0.0.0.0", port=config.get("api_port"))
  237. # HTTP API线程并启动
  238. inside_http_api_thread = threading.Thread(target=http_api_thread)
  239. inside_http_api_thread.start()
  240. # 添加用户名到最新的用户名列表
  241. def add_username_to_last_username_list(data):
  242. """
  243. data(str): 用户名
  244. """
  245. global last_username_list
  246. # 添加数据到 最新入场的用户名列表
  247. last_username_list.append(data)
  248. # 保留最新的3个数据
  249. last_username_list = last_username_list[-3:]
  250. """
  251. 按键监听板块
  252. """
  253. # 录音功能(录音时间过短进入openai的语音转文字会报错,请一定注意)
  254. def record_audio():
  255. pressdown_num = 0
  256. CHUNK = 1024
  257. FORMAT = pyaudio.paInt16
  258. CHANNELS = 1
  259. RATE = 44100
  260. WAVE_OUTPUT_FILENAME = "out/record.wav"
  261. p = pyaudio.PyAudio()
  262. stream = p.open(
  263. format=FORMAT,
  264. channels=CHANNELS,
  265. rate=RATE,
  266. input=True,
  267. frames_per_buffer=CHUNK,
  268. )
  269. frames = []
  270. logger.info("Recording...")
  271. flag = 0
  272. while 1:
  273. while keyboard.is_pressed("RIGHT_SHIFT"):
  274. flag = 1
  275. data = stream.read(CHUNK)
  276. frames.append(data)
  277. pressdown_num = pressdown_num + 1
  278. if flag:
  279. break
  280. logger.info("Stopped recording.")
  281. stream.stop_stream()
  282. stream.close()
  283. p.terminate()
  284. wf = wave.open(WAVE_OUTPUT_FILENAME, "wb")
  285. wf.setnchannels(CHANNELS)
  286. wf.setsampwidth(p.get_sample_size(FORMAT))
  287. wf.setframerate(RATE)
  288. wf.writeframes(b"".join(frames))
  289. wf.close()
  290. if pressdown_num >= 5: # 粗糙的处理手段
  291. return 1
  292. else:
  293. logger.info("杂鱼杂鱼,好短好短(录音时间过短,按右shift重新录制)")
  294. return 0
  295. # THRESHOLD 设置音量阈值,默认值800.0,根据实际情况调整 silence_threshold 设置沉默阈值,根据实际情况调整
  296. def audio_listen(volume_threshold=800.0, silence_threshold=15):
  297. audio = pyaudio.PyAudio()
  298. # 设置音频参数
  299. FORMAT = pyaudio.paInt16
  300. CHANNELS = config.get("talk", "CHANNELS")
  301. RATE = config.get("talk", "RATE")
  302. CHUNK = 1024
  303. stream = audio.open(
  304. format=FORMAT,
  305. channels=CHANNELS,
  306. rate=RATE,
  307. input=True,
  308. frames_per_buffer=CHUNK,
  309. input_device_index=int(config.get("talk", "device_index")),
  310. )
  311. frames = [] # 存储录制的音频帧
  312. is_speaking = False # 是否在说话
  313. silent_count = 0 # 沉默计数
  314. speaking_flag = False # 录入标志位 不重要
  315. logger.info("[即将开始录音……]")
  316. while True:
  317. # 播放中不录音
  318. if config.get("talk", "no_recording_during_playback"):
  319. # 存在待合成音频 或 已合成音频还未播放 或 播放中 或 在数据处理中
  320. if (
  321. my_handle.is_audio_queue_empty() != 15
  322. or my_handle.is_handle_empty() == 1
  323. or wait_play_audio_num > 0
  324. ):
  325. time.sleep(
  326. float(
  327. config.get(
  328. "talk", "no_recording_during_playback_sleep_interval"
  329. )
  330. )
  331. )
  332. continue
  333. # 读取音频数据
  334. data = stream.read(CHUNK)
  335. audio_data = np.frombuffer(data, dtype=np.short)
  336. max_dB = np.max(audio_data)
  337. # logger.info(max_dB)
  338. if max_dB > volume_threshold:
  339. is_speaking = True
  340. silent_count = 0
  341. elif is_speaking is True:
  342. silent_count += 1
  343. if is_speaking is True:
  344. frames.append(data)
  345. if speaking_flag is False:
  346. logger.info("[录入中……]")
  347. speaking_flag = True
  348. if silent_count >= silence_threshold:
  349. break
  350. logger.info("[语音录入完成]")
  351. # 将音频保存为WAV文件
  352. """with wave.open(WAVE_OUTPUT_FILENAME, 'wb') as wf:
  353. wf.setnchannels(CHANNELS)
  354. wf.setsampwidth(pyaudio.get_sample_size(FORMAT))
  355. wf.setframerate(RATE)
  356. wf.writeframes(b''.join(frames))"""
  357. return frames
  358. # 处理聊天逻辑
  359. def talk_handle(content: str):
  360. global is_talk_awake
  361. try:
  362. # 检查并切换聊天唤醒状态
  363. def check_talk_awake(content: str):
  364. """检查并切换聊天唤醒状态
  365. Args:
  366. content (str): 聊天内容
  367. Returns:
  368. dict:
  369. ret 是否需要触发
  370. is_talk_awake 当前唤醒状态
  371. first 是否是第一次触发 唤醒or睡眠,用于触发首次切换时的特殊提示语
  372. """
  373. global is_talk_awake
  374. # 判断是否启动了 唤醒词功能
  375. if config.get("talk", "wakeup_sleep", "enable"):
  376. if config.get("talk", "wakeup_sleep", "mode") == "长期唤醒":
  377. # 判断现在是否是唤醒状态
  378. if is_talk_awake is False:
  379. # 判断文本内容是否包含唤醒词
  380. trigger_word = common.find_substring_in_list(
  381. content, config.get("talk", "wakeup_sleep", "wakeup_word")
  382. )
  383. if trigger_word:
  384. is_talk_awake = True
  385. logger.info("[聊天唤醒成功]")
  386. return {
  387. "ret": 0,
  388. "is_talk_awake": is_talk_awake,
  389. "first": True,
  390. "trigger_word": trigger_word,
  391. }
  392. return {
  393. "ret": -1,
  394. "is_talk_awake": is_talk_awake,
  395. "first": False,
  396. }
  397. else:
  398. # 判断文本内容是否包含睡眠词
  399. trigger_word = common.find_substring_in_list(
  400. content, config.get("talk", "wakeup_sleep", "sleep_word")
  401. )
  402. if trigger_word:
  403. is_talk_awake = False
  404. logger.info("[聊天睡眠成功]")
  405. return {
  406. "ret": 0,
  407. "is_talk_awake": is_talk_awake,
  408. "first": True,
  409. "trigger_word": trigger_word,
  410. }
  411. return {
  412. "ret": 0,
  413. "is_talk_awake": is_talk_awake,
  414. "first": False,
  415. }
  416. elif config.get("talk", "wakeup_sleep", "mode") == "单次唤醒":
  417. # 无需判断当前是否是唤醒状态,因为默认都是状态清除
  418. # 判断文本内容是否包含唤醒词
  419. trigger_word = common.find_substring_in_list(
  420. content, config.get("talk", "wakeup_sleep", "wakeup_word")
  421. )
  422. if trigger_word:
  423. is_talk_awake = True
  424. logger.info("[聊天唤醒成功]")
  425. return {
  426. "ret": 0,
  427. "is_talk_awake": is_talk_awake,
  428. # 单次唤醒下 没有首次唤醒提示
  429. "first": False,
  430. "trigger_word": trigger_word,
  431. }
  432. return {
  433. "ret": -1,
  434. "is_talk_awake": is_talk_awake,
  435. "first": False,
  436. }
  437. return {"ret": 0, "is_talk_awake": True, "trigger_word": "", "first": False}
  438. # 输出识别结果
  439. logger.info("识别结果:" + content)
  440. # 空内容过滤
  441. if content == "":
  442. return
  443. username = config.get("talk", "username")
  444. data = {"platform": "本地聊天", "username": username, "content": content}
  445. # 检查并切换聊天唤醒状态
  446. check_resp = check_talk_awake(content)
  447. if check_resp["ret"] == 0:
  448. # 唤醒情况下
  449. if check_resp["is_talk_awake"]:
  450. # 长期唤醒、且不是首次触发的情况下,后面的内容不会携带触发词,即使携带了也不应该进行替换操作
  451. if config.get("talk", "wakeup_sleep", "mode") == "长期唤醒" and not check_resp["first"]:
  452. pass
  453. else:
  454. # 替换触发词为空
  455. content = content.replace(check_resp["trigger_word"], "").strip()
  456. # 因为唤醒可能会有仅唤醒词的情况,所以可能出现首次唤醒,唤醒词被过滤,content为空清空,导致不播放唤醒提示语,需要处理
  457. if content == "" and not check_resp["first"]:
  458. return
  459. # 赋值给data
  460. data["content"] = content
  461. # 首次触发切换模式
  462. if check_resp["first"]:
  463. # 随机获取文案 TODO: 如果此功能测试成功,所有的类似功能都将使用此函数简化代码
  464. resp_json = common.get_random_str_in_list_and_format(
  465. ori_list=config.get(
  466. "talk", "wakeup_sleep", "wakeup_copywriting"
  467. )
  468. )
  469. if resp_json["ret"] == 0:
  470. data["content"] = resp_json["content"]
  471. data["insert_index"] = -1
  472. my_handle.reread_handle(data)
  473. else:
  474. my_handle.process_data(data, "talk")
  475. # 单次唤醒情况下,唤醒后关闭
  476. if config.get("talk", "wakeup_sleep", "mode") == "单次唤醒":
  477. is_talk_awake = False
  478. else:
  479. if check_resp["first"]:
  480. resp_json = common.get_random_str_in_list_and_format(
  481. ori_list=config.get(
  482. "talk", "wakeup_sleep", "sleep_copywriting"
  483. )
  484. )
  485. if resp_json["ret"] == 0:
  486. data["content"] = resp_json["content"]
  487. data["insert_index"] = -1
  488. my_handle.reread_handle(data)
  489. except Exception as e:
  490. logger.error(traceback.format_exc())
  491. # 执行录音、识别&提交
  492. def do_listen_and_comment(status=True):
  493. global \
  494. stop_do_listen_and_comment_thread_event, \
  495. faster_whisper_model, \
  496. sense_voice_model, \
  497. is_recording, \
  498. is_talk_awake
  499. try:
  500. is_recording = True
  501. config = Config(config_path)
  502. # 是否启用按键监听和直接对话,没启用的话就不用执行了
  503. if not config.get("talk", "key_listener_enable") and not config.get("talk", "direct_run_talk"):
  504. is_recording = False
  505. return
  506. # 针对faster_whisper情况,模型加载一次共用,减少开销
  507. if "faster_whisper" == config.get("talk", "type"):
  508. from faster_whisper import WhisperModel
  509. if faster_whisper_model is None:
  510. logger.info("faster_whisper 模型加载中,请稍后...")
  511. # Run on GPU with FP16
  512. faster_whisper_model = WhisperModel(
  513. model_size_or_path=config.get(
  514. "talk", "faster_whisper", "model_size"
  515. ),
  516. device=config.get("talk", "faster_whisper", "device"),
  517. compute_type=config.get(
  518. "talk", "faster_whisper", "compute_type"
  519. ),
  520. download_root=config.get(
  521. "talk", "faster_whisper", "download_root"
  522. ),
  523. )
  524. logger.info("faster_whisper 模型加载完毕,可以开始说话了喵~")
  525. elif "sensevoice" == config.get("talk", "type"):
  526. from funasr import AutoModel
  527. logger.info("sensevoice 模型加载中,请稍后...")
  528. asr_model_path = config.get("talk", "sensevoice", "asr_model_path")
  529. vad_model_path = config.get("talk", "sensevoice", "vad_model_path")
  530. if sense_voice_model is None:
  531. sense_voice_model = AutoModel(
  532. model=asr_model_path,
  533. vad_model=vad_model_path,
  534. vad_kwargs={
  535. "max_single_segment_time": int(
  536. config.get(
  537. "talk", "sensevoice", "vad_max_single_segment_time"
  538. )
  539. )
  540. },
  541. trust_remote_code=True,
  542. device=config.get("talk", "sensevoice", "device"),
  543. remote_code="./sensevoice/model.py",
  544. )
  545. logger.info("sensevoice 模型加载完毕,可以开始说话了喵~")
  546. while True:
  547. try:
  548. # 检查是否收到停止事件
  549. if stop_do_listen_and_comment_thread_event.is_set():
  550. logger.info("停止录音~")
  551. is_recording = False
  552. break
  553. config = Config(config_path)
  554. # 根据接入的语音识别类型执行
  555. if config.get("talk", "type") in [
  556. "baidu",
  557. "faster_whisper",
  558. "sensevoice",
  559. ]:
  560. # 设置音频参数
  561. FORMAT = pyaudio.paInt16
  562. CHANNELS = config.get("talk", "CHANNELS")
  563. RATE = config.get("talk", "RATE")
  564. audio_out_path = config.get("play_audio", "out_path")
  565. if not os.path.isabs(audio_out_path):
  566. if not audio_out_path.startswith("./"):
  567. audio_out_path = "./" + audio_out_path
  568. file_name = "asr_" + common.get_bj_time(4) + ".wav"
  569. WAVE_OUTPUT_FILENAME = common.get_new_audio_path(
  570. audio_out_path, file_name
  571. )
  572. # WAVE_OUTPUT_FILENAME = './out/asr_' + common.get_bj_time(4) + '.wav'
  573. frames = audio_listen(
  574. config.get("talk", "volume_threshold"),
  575. config.get("talk", "silence_threshold"),
  576. )
  577. # 将音频保存为WAV文件
  578. with wave.open(WAVE_OUTPUT_FILENAME, "wb") as wf:
  579. wf.setnchannels(CHANNELS)
  580. wf.setsampwidth(pyaudio.get_sample_size(FORMAT))
  581. wf.setframerate(RATE)
  582. wf.writeframes(b"".join(frames))
  583. if config.get("talk", "type") == "baidu":
  584. # 读取音频文件
  585. with open(WAVE_OUTPUT_FILENAME, "rb") as fp:
  586. audio = fp.read()
  587. # 初始化 AipSpeech 对象
  588. baidu_client = AipSpeech(
  589. config.get("talk", "baidu", "app_id"),
  590. config.get("talk", "baidu", "api_key"),
  591. config.get("talk", "baidu", "secret_key"),
  592. )
  593. # 识别音频文件
  594. res = baidu_client.asr(
  595. audio,
  596. "wav",
  597. 16000,
  598. {
  599. "dev_pid": 1536,
  600. },
  601. )
  602. if res["err_no"] == 0:
  603. content = res["result"][0]
  604. talk_handle(content)
  605. else:
  606. logger.error(f"百度接口报错:{res}")
  607. elif config.get("talk", "type") == "faster_whisper":
  608. logger.debug("faster_whisper模型加载中...")
  609. language = config.get("talk", "faster_whisper", "language")
  610. if language == "自动识别":
  611. language = None
  612. segments, info = faster_whisper_model.transcribe(
  613. WAVE_OUTPUT_FILENAME,
  614. language=language,
  615. beam_size=config.get(
  616. "talk", "faster_whisper", "beam_size"
  617. ),
  618. )
  619. logger.debug(
  620. "识别语言为:'%s',概率:%f"
  621. % (info.language, info.language_probability)
  622. )
  623. content = ""
  624. for segment in segments:
  625. logger.info(
  626. "[%.2fs -> %.2fs] %s"
  627. % (segment.start, segment.end, segment.text)
  628. )
  629. content += segment.text + "。"
  630. if content == "":
  631. # 恢复录音标志位
  632. is_recording = False
  633. return
  634. talk_handle(content)
  635. elif config.get("talk", "type") == "sensevoice":
  636. res = sense_voice_model.generate(
  637. input=WAVE_OUTPUT_FILENAME,
  638. cache={},
  639. language=config.get("talk", "sensevoice", "language"),
  640. text_norm=config.get("talk", "sensevoice", "text_norm"),
  641. batch_size_s=int(
  642. config.get("talk", "sensevoice", "batch_size_s")
  643. ),
  644. batch_size=int(
  645. config.get("talk", "sensevoice", "batch_size")
  646. ),
  647. )
  648. def remove_angle_brackets_content(input_string: str):
  649. # 使用正则表达式来匹配并删除 <> 之间的内容
  650. return re.sub(r"<.*?>", "", input_string)
  651. content = remove_angle_brackets_content(res[0]["text"])
  652. talk_handle(content)
  653. elif "google" == config.get("talk", "type"):
  654. # 创建Recognizer对象
  655. r = sr.Recognizer()
  656. try:
  657. # 打开麦克风进行录音
  658. with sr.Microphone() as source:
  659. logger.info("录音中...")
  660. # 从麦克风获取音频数据
  661. audio = r.listen(source)
  662. logger.info("成功录制")
  663. # 进行谷歌实时语音识别 en-US zh-CN ja-JP
  664. content = r.recognize_google(
  665. audio,
  666. language=config.get("talk", "google", "tgt_lang"),
  667. )
  668. talk_handle(content)
  669. except sr.UnknownValueError:
  670. logger.warning("无法识别输入的语音")
  671. except sr.RequestError as e:
  672. logger.error("请求出错:" + str(e))
  673. is_recording = False
  674. if not status:
  675. return
  676. except Exception as e:
  677. logger.error(traceback.format_exc())
  678. is_recording = False
  679. return
  680. except Exception as e:
  681. logger.error(traceback.format_exc())
  682. is_recording = False
  683. return
  684. def on_key_press(event):
  685. global \
  686. do_listen_and_comment_thread, \
  687. stop_do_listen_and_comment_thread_event, \
  688. is_recording
  689. # 是否启用按键监听,不启用的话就不用执行了
  690. if not config.get("talk", "key_listener_enable"):
  691. return
  692. # if event.name in ['z', 'Z', 'c', 'C'] and keyboard.is_pressed('ctrl'):
  693. # logger.info("退出程序")
  694. # os._exit(0)
  695. # 按键CD
  696. current_time = time.time()
  697. if current_time - last_pressed < cooldown:
  698. return
  699. """
  700. 触发按键部分的判断
  701. """
  702. trigger_key_lower = None
  703. stop_trigger_key_lower = None
  704. # trigger_key是字母, 整个小写
  705. if trigger_key.isalpha():
  706. trigger_key_lower = trigger_key.lower()
  707. # stop_trigger_key是字母, 整个小写
  708. if stop_trigger_key.isalpha():
  709. stop_trigger_key_lower = stop_trigger_key.lower()
  710. if trigger_key_lower:
  711. if event.name == trigger_key or event.name == trigger_key_lower:
  712. logger.info(f"检测到单击键盘 {event.name},即将开始录音~")
  713. elif event.name == stop_trigger_key or event.name == stop_trigger_key_lower:
  714. logger.info(f"检测到单击键盘 {event.name},即将停止录音~")
  715. stop_do_listen_and_comment_thread_event.set()
  716. return
  717. else:
  718. return
  719. else:
  720. if event.name == trigger_key:
  721. logger.info(f"检测到单击键盘 {event.name},即将开始录音~")
  722. elif event.name == stop_trigger_key:
  723. logger.info(f"检测到单击键盘 {event.name},即将停止录音~")
  724. stop_do_listen_and_comment_thread_event.set()
  725. return
  726. else:
  727. return
  728. if not is_recording:
  729. # 是否启用连续对话模式
  730. if config.get("talk", "continuous_talk"):
  731. stop_do_listen_and_comment_thread_event.clear()
  732. do_listen_and_comment_thread = threading.Thread(
  733. target=do_listen_and_comment, args=(True,)
  734. )
  735. do_listen_and_comment_thread.start()
  736. else:
  737. stop_do_listen_and_comment_thread_event.clear()
  738. do_listen_and_comment_thread = threading.Thread(
  739. target=do_listen_and_comment, args=(False,)
  740. )
  741. do_listen_and_comment_thread.start()
  742. else:
  743. logger.warning("正在录音中...请勿重复点击录音捏!")
  744. # 按键监听
  745. def key_listener():
  746. # 注册按键按下事件的回调函数
  747. keyboard.on_press(on_key_press)
  748. try:
  749. # 进入监听状态,等待按键按下
  750. keyboard.wait()
  751. except KeyboardInterrupt:
  752. os._exit(0)
  753. # 直接运行语音对话
  754. def direct_run_talk():
  755. global \
  756. do_listen_and_comment_thread, \
  757. stop_do_listen_and_comment_thread_event, \
  758. is_recording
  759. if not is_recording:
  760. # 是否启用连续对话模式
  761. if config.get("talk", "continuous_talk"):
  762. stop_do_listen_and_comment_thread_event.clear()
  763. do_listen_and_comment_thread = threading.Thread(
  764. target=do_listen_and_comment, args=(True,)
  765. )
  766. do_listen_and_comment_thread.start()
  767. else:
  768. stop_do_listen_and_comment_thread_event.clear()
  769. do_listen_and_comment_thread = threading.Thread(
  770. target=do_listen_and_comment, args=(False,)
  771. )
  772. do_listen_and_comment_thread.start()
  773. # 从配置文件中读取触发键的字符串配置
  774. trigger_key = config.get("talk", "trigger_key")
  775. stop_trigger_key = config.get("talk", "stop_trigger_key")
  776. # 是否启用了 按键监听
  777. if config.get("talk", "key_listener_enable"):
  778. logger.info(
  779. f"单击键盘 {trigger_key} 按键进行录音喵~ 由于其他任务还要启动,如果按键没有反应,请等待一段时间(如果使用本地ASR,请等待模型加载完成后使用)"
  780. )
  781. # 是否启用了直接运行对话,如果启用了,将在首次运行时直接进行语音识别,而不需手动点击开始按键。针对有些系统按键无法触发的情况下,配合连续对话和唤醒词使用
  782. if config.get("talk", "direct_run_talk"):
  783. logger.info("直接运行对话模式,首次运行时将直接进行语音识别,而不需手动点击开始按键(如果使用本地ASR,请等待模型加载完成后使用)")
  784. direct_run_talk()
  785. # 创建并启动按键监听线程,放着也是在聊天模式下,让程序一直阻塞用的
  786. thread = threading.Thread(target=key_listener)
  787. thread.start()
  788. # 定时任务
  789. def schedule_task(index):
  790. global config, common, my_handle, last_liveroom_data, last_username_list
  791. logger.debug("定时任务执行中...")
  792. hour, min = common.get_bj_time(6)
  793. if 0 <= hour and hour < 6:
  794. time = f"凌晨{hour}点{min}分"
  795. elif 6 <= hour and hour < 9:
  796. time = f"早晨{hour}点{min}分"
  797. elif 9 <= hour and hour < 12:
  798. time = f"上午{hour}点{min}分"
  799. elif hour == 12:
  800. time = f"中午{hour}点{min}分"
  801. elif 13 <= hour and hour < 18:
  802. time = f"下午{hour - 12}点{min}分"
  803. elif 18 <= hour and hour < 20:
  804. time = f"傍晚{hour - 12}点{min}分"
  805. elif 20 <= hour and hour < 24:
  806. time = f"晚上{hour - 12}点{min}分"
  807. # 根据对应索引从列表中随机获取一个值
  808. if len(config.get("schedule")[index]["copy"]) <= 0:
  809. return None
  810. random_copy = random.choice(config.get("schedule")[index]["copy"])
  811. # 假设有多个未知变量,用户可以在此处定义动态变量
  812. variables = {
  813. "time": time,
  814. "user_num": "N",
  815. "last_username": last_username_list[-1],
  816. }
  817. # 有用户数据情况的平台特殊处理
  818. if platform in ["dy", "tiktok"]:
  819. variables["user_num"] = last_liveroom_data["OnlineUserCount"]
  820. # 使用字典进行字符串替换
  821. if any(var in random_copy for var in variables):
  822. content = random_copy.format(
  823. **{var: value for var, value in variables.items() if var in random_copy}
  824. )
  825. else:
  826. content = random_copy
  827. content = common.brackets_text_randomize(content)
  828. data = {"platform": platform, "username": "定时任务", "content": content}
  829. logger.info(f"定时任务:{content}")
  830. my_handle.process_data(data, "schedule")
  831. # schedule.clear(index)
  832. # 启动定时任务
  833. def run_schedule():
  834. global config
  835. try:
  836. for index, task in enumerate(config.get("schedule")):
  837. if task["enable"]:
  838. # logger.info(task)
  839. min_seconds = int(task["time_min"])
  840. max_seconds = int(task["time_max"])
  841. def schedule_random_task(index, min_seconds, max_seconds):
  842. schedule.clear(index)
  843. # 在min_seconds和max_seconds之间随机选择下一次任务执行的时间
  844. next_time = random.randint(min_seconds, max_seconds)
  845. # logger.info(f"Next task {index} scheduled in {next_time} seconds at {time.ctime()}")
  846. schedule_task(index)
  847. schedule.every(next_time).seconds.do(
  848. schedule_random_task, index, min_seconds, max_seconds
  849. ).tag(index)
  850. schedule_random_task(index, min_seconds, max_seconds)
  851. except Exception as e:
  852. logger.error(traceback.format_exc())
  853. while True:
  854. schedule.run_pending()
  855. # time.sleep(1) # 控制每次循环的间隔时间,避免过多占用 CPU 资源
  856. # 创建定时任务子线程并启动 在平台是 dy的情况下,默认启动定时任务用于阻塞
  857. if any(item["enable"] for item in config.get("schedule")) or platform == "dy":
  858. # 创建定时任务子线程并启动
  859. schedule_thread = threading.Thread(target=run_schedule)
  860. schedule_thread.start()
  861. # 启动动态文案
  862. async def run_trends_copywriting():
  863. global config
  864. try:
  865. if not config.get("trends_copywriting", "enable"):
  866. return
  867. logger.info("动态文案任务线程运行中...")
  868. while True:
  869. # 文案文件路径列表
  870. copywriting_file_path_list = []
  871. # 获取动态文案列表
  872. for copywriting in config.get("trends_copywriting", "copywriting"):
  873. # 获取文件夹内所有文件的文件绝对路径,包括文件扩展名
  874. for tmp in common.get_all_file_paths(copywriting["folder_path"]):
  875. copywriting_file_path_list.append(tmp)
  876. # 是否开启随机播放
  877. if config.get("trends_copywriting", "random_play"):
  878. random.shuffle(copywriting_file_path_list)
  879. logger.debug(
  880. f"copywriting_file_path_list={copywriting_file_path_list}"
  881. )
  882. # 遍历文案文件路径列表
  883. for copywriting_file_path in copywriting_file_path_list:
  884. # 获取文案文件内容
  885. copywriting_file_content = common.read_file_return_content(
  886. copywriting_file_path
  887. )
  888. # 是否启用提示词对文案内容进行转换
  889. if copywriting["prompt_change_enable"]:
  890. data_json = {
  891. "username": "trends_copywriting",
  892. "content": copywriting["prompt_change_content"]
  893. + copywriting_file_content,
  894. }
  895. # 调用函数进行LLM处理,以及生成回复内容,进行音频合成,需要好好考虑考虑实现
  896. data_json["content"] = my_handle.llm_handle(
  897. config.get("trends_copywriting", "llm_type"), data_json
  898. )
  899. else:
  900. copywriting_file_content = common.brackets_text_randomize(
  901. copywriting_file_content
  902. )
  903. data_json = {
  904. "username": "trends_copywriting",
  905. "content": copywriting_file_content,
  906. }
  907. logger.debug(
  908. f'copywriting_file_content={copywriting_file_content},content={data_json["content"]}'
  909. )
  910. # 空数据判断
  911. if (
  912. data_json["content"] is not None
  913. and data_json["content"] != ""
  914. ):
  915. # 发给直接复读进行处理
  916. my_handle.reread_handle(
  917. data_json, filter=True, type="trends_copywriting"
  918. )
  919. await asyncio.sleep(
  920. config.get("trends_copywriting", "play_interval")
  921. )
  922. except Exception as e:
  923. logger.error(traceback.format_exc())
  924. if config.get("trends_copywriting", "enable"):
  925. # 创建动态文案子线程并启动
  926. threading.Thread(target=lambda: asyncio.run(run_trends_copywriting())).start()
  927. # 闲时任务
  928. async def idle_time_task():
  929. global config, global_idle_time, common
  930. try:
  931. if not config.get("idle_time_task", "enable"):
  932. return
  933. logger.info("闲时任务线程运行中...")
  934. # 记录上一次触发的任务类型
  935. last_mode = 0
  936. copywriting_copy_list = None
  937. comment_copy_list = None
  938. local_audio_path_list = None
  939. overflow_time_min = int(config.get("idle_time_task", "idle_time_min"))
  940. overflow_time_max = int(config.get("idle_time_task", "idle_time_max"))
  941. overflow_time = random.randint(overflow_time_min, overflow_time_max)
  942. logger.info(f"下一个闲时任务将在{overflow_time}秒后执行")
  943. def load_data_list(type):
  944. if type == "copywriting":
  945. tmp = config.get("idle_time_task", "copywriting", "copy")
  946. elif type == "comment":
  947. tmp = config.get("idle_time_task", "comment", "copy")
  948. elif type == "local_audio":
  949. tmp = config.get("idle_time_task", "local_audio", "path")
  950. logger.debug(f"type={type}, tmp={tmp}")
  951. tmp2 = copy.copy(tmp)
  952. return tmp2
  953. # 加载数据到list
  954. copywriting_copy_list = load_data_list("copywriting")
  955. comment_copy_list = load_data_list("comment")
  956. local_audio_path_list = load_data_list("local_audio")
  957. logger.debug(f"copywriting_copy_list={copywriting_copy_list}")
  958. logger.debug(f"comment_copy_list={comment_copy_list}")
  959. logger.debug(f"local_audio_path_list={local_audio_path_list}")
  960. def do_task(
  961. last_mode,
  962. copywriting_copy_list,
  963. comment_copy_list,
  964. local_audio_path_list,
  965. ):
  966. global global_idle_time
  967. # 闲时计数清零
  968. global_idle_time = 0
  969. # 闲时任务处理
  970. if config.get("idle_time_task", "copywriting", "enable"):
  971. if last_mode == 0:
  972. # 是否开启了随机触发
  973. if config.get("idle_time_task", "copywriting", "random"):
  974. logger.debug("切换到文案触发模式")
  975. if copywriting_copy_list != []:
  976. # 随机打乱列表中的元素
  977. random.shuffle(copywriting_copy_list)
  978. copywriting_copy = copywriting_copy_list.pop(0)
  979. else:
  980. # 刷新list数据
  981. copywriting_copy_list = load_data_list("copywriting")
  982. # 随机打乱列表中的元素
  983. random.shuffle(copywriting_copy_list)
  984. if copywriting_copy_list != []:
  985. copywriting_copy = copywriting_copy_list.pop(0)
  986. else:
  987. return (
  988. last_mode,
  989. copywriting_copy_list,
  990. comment_copy_list,
  991. local_audio_path_list,
  992. )
  993. else:
  994. logger.debug(copywriting_copy_list)
  995. if copywriting_copy_list != []:
  996. copywriting_copy = copywriting_copy_list.pop(0)
  997. else:
  998. # 刷新list数据
  999. copywriting_copy_list = load_data_list("copywriting")
  1000. if copywriting_copy_list != []:
  1001. copywriting_copy = copywriting_copy_list.pop(0)
  1002. else:
  1003. return (
  1004. last_mode,
  1005. copywriting_copy_list,
  1006. comment_copy_list,
  1007. local_audio_path_list,
  1008. )
  1009. hour, min = common.get_bj_time(6)
  1010. if 0 <= hour and hour < 6:
  1011. time = f"凌晨{hour}点{min}分"
  1012. elif 6 <= hour and hour < 9:
  1013. time = f"早晨{hour}点{min}分"
  1014. elif 9 <= hour and hour < 12:
  1015. time = f"上午{hour}点{min}分"
  1016. elif hour == 12:
  1017. time = f"中午{hour}点{min}分"
  1018. elif 13 <= hour and hour < 18:
  1019. time = f"下午{hour - 12}点{min}分"
  1020. elif 18 <= hour and hour < 20:
  1021. time = f"傍晚{hour - 12}点{min}分"
  1022. elif 20 <= hour and hour < 24:
  1023. time = f"晚上{hour - 12}点{min}分"
  1024. # 动态变量替换
  1025. # 假设有多个未知变量,用户可以在此处定义动态变量
  1026. variables = {
  1027. "time": time,
  1028. "user_num": "N",
  1029. "last_username": last_username_list[-1],
  1030. }
  1031. # 有用户数据情况的平台特殊处理
  1032. if platform in ["dy", "tiktok"]:
  1033. variables["user_num"] = last_liveroom_data[
  1034. "OnlineUserCount"
  1035. ]
  1036. # 使用字典进行字符串替换
  1037. if any(var in copywriting_copy for var in variables):
  1038. copywriting_copy = copywriting_copy.format(
  1039. **{
  1040. var: value
  1041. for var, value in variables.items()
  1042. if var in copywriting_copy
  1043. }
  1044. )
  1045. # [1|2]括号语法随机获取一个值,返回取值完成后的字符串
  1046. copywriting_copy = common.brackets_text_randomize(
  1047. copywriting_copy
  1048. )
  1049. # 发送给处理函数
  1050. data = {
  1051. "platform": platform,
  1052. "username": "闲时任务-文案模式",
  1053. "type": "reread",
  1054. "content": copywriting_copy,
  1055. }
  1056. my_handle.process_data(data, "idle_time_task")
  1057. # 模式切换
  1058. last_mode = 1
  1059. overflow_time = random.randint(
  1060. overflow_time_min, overflow_time_max
  1061. )
  1062. logger.info(f"下一个闲时任务将在{overflow_time}秒后执行")
  1063. return (
  1064. last_mode,
  1065. copywriting_copy_list,
  1066. comment_copy_list,
  1067. local_audio_path_list,
  1068. )
  1069. else:
  1070. last_mode = 1
  1071. if config.get("idle_time_task", "comment", "enable"):
  1072. if last_mode == 1:
  1073. # 是否开启了随机触发
  1074. if config.get("idle_time_task", "comment", "random"):
  1075. logger.debug("切换到弹幕触发LLM模式")
  1076. if comment_copy_list != []:
  1077. # 随机打乱列表中的元素
  1078. random.shuffle(comment_copy_list)
  1079. comment_copy = comment_copy_list.pop(0)
  1080. else:
  1081. # 刷新list数据
  1082. comment_copy_list = load_data_list("comment")
  1083. # 随机打乱列表中的元素
  1084. random.shuffle(comment_copy_list)
  1085. comment_copy = comment_copy_list.pop(0)
  1086. else:
  1087. if comment_copy_list != []:
  1088. comment_copy = comment_copy_list.pop(0)
  1089. else:
  1090. # 刷新list数据
  1091. comment_copy_list = load_data_list("comment")
  1092. comment_copy = comment_copy_list.pop(0)
  1093. hour, min = common.get_bj_time(6)
  1094. if 0 <= hour and hour < 6:
  1095. time = f"凌晨{hour}点{min}分"
  1096. elif 6 <= hour and hour < 9:
  1097. time = f"早晨{hour}点{min}分"
  1098. elif 9 <= hour and hour < 12:
  1099. time = f"上午{hour}点{min}分"
  1100. elif hour == 12:
  1101. time = f"中午{hour}点{min}分"
  1102. elif 13 <= hour and hour < 18:
  1103. time = f"下午{hour - 12}点{min}分"
  1104. elif 18 <= hour and hour < 20:
  1105. time = f"傍晚{hour - 12}点{min}分"
  1106. elif 20 <= hour and hour < 24:
  1107. time = f"晚上{hour - 12}点{min}分"
  1108. # 动态变量替换
  1109. # 假设有多个未知变量,用户可以在此处定义动态变量
  1110. variables = {
  1111. "time": time,
  1112. "user_num": "N",
  1113. "last_username": last_username_list[-1],
  1114. }
  1115. # 有用户数据情况的平台特殊处理
  1116. if platform in ["dy", "tiktok"]:
  1117. variables["user_num"] = last_liveroom_data[
  1118. "OnlineUserCount"
  1119. ]
  1120. # 使用字典进行字符串替换
  1121. if any(var in comment_copy for var in variables):
  1122. comment_copy = comment_copy.format(
  1123. **{
  1124. var: value
  1125. for var, value in variables.items()
  1126. if var in comment_copy
  1127. }
  1128. )
  1129. # [1|2]括号语法随机获取一个值,返回取值完成后的字符串
  1130. comment_copy = common.brackets_text_randomize(comment_copy)
  1131. # 发送给处理函数
  1132. data = {
  1133. "platform": platform,
  1134. "username": "闲时任务-弹幕触发LLM模式",
  1135. "type": "comment",
  1136. "content": comment_copy,
  1137. }
  1138. my_handle.process_data(data, "idle_time_task")
  1139. # 模式切换
  1140. last_mode = 2
  1141. overflow_time = random.randint(
  1142. overflow_time_min, overflow_time_max
  1143. )
  1144. logger.info(f"下一个闲时任务将在{overflow_time}秒后执行")
  1145. return (
  1146. last_mode,
  1147. copywriting_copy_list,
  1148. comment_copy_list,
  1149. local_audio_path_list,
  1150. )
  1151. else:
  1152. last_mode = 2
  1153. if config.get("idle_time_task", "local_audio", "enable"):
  1154. if last_mode == 2:
  1155. logger.debug("切换到本地音频模式")
  1156. # 是否开启了随机触发
  1157. if config.get("idle_time_task", "local_audio", "random"):
  1158. if local_audio_path_list != []:
  1159. # 随机打乱列表中的元素
  1160. random.shuffle(local_audio_path_list)
  1161. local_audio_path = local_audio_path_list.pop(0)
  1162. else:
  1163. # 刷新list数据
  1164. local_audio_path_list = load_data_list("local_audio")
  1165. # 随机打乱列表中的元素
  1166. random.shuffle(local_audio_path_list)
  1167. local_audio_path = local_audio_path_list.pop(0)
  1168. else:
  1169. if local_audio_path_list != []:
  1170. local_audio_path = local_audio_path_list.pop(0)
  1171. else:
  1172. # 刷新list数据
  1173. local_audio_path_list = load_data_list("local_audio")
  1174. local_audio_path = local_audio_path_list.pop(0)
  1175. # [1|2]括号语法随机获取一个值,返回取值完成后的字符串
  1176. local_audio_path = common.brackets_text_randomize(
  1177. local_audio_path
  1178. )
  1179. logger.debug(f"local_audio_path={local_audio_path}")
  1180. # 发送给处理函数
  1181. data = {
  1182. "platform": platform,
  1183. "username": "闲时任务-本地音频模式",
  1184. "type": "local_audio",
  1185. "content": common.extract_filename(local_audio_path, False),
  1186. "file_path": local_audio_path,
  1187. }
  1188. my_handle.process_data(data, "idle_time_task")
  1189. # 模式切换
  1190. last_mode = 0
  1191. overflow_time = random.randint(
  1192. overflow_time_min, overflow_time_max
  1193. )
  1194. logger.info(f"下一个闲时任务将在{overflow_time}秒后执行")
  1195. return (
  1196. last_mode,
  1197. copywriting_copy_list,
  1198. comment_copy_list,
  1199. local_audio_path_list,
  1200. )
  1201. else:
  1202. last_mode = 0
  1203. return (
  1204. last_mode,
  1205. copywriting_copy_list,
  1206. comment_copy_list,
  1207. local_audio_path_list,
  1208. )
  1209. while True:
  1210. # 如果闲时时间范围为0,就睡眠100ms 意思意思
  1211. if overflow_time_min > 0 and overflow_time_min > 0:
  1212. # 每隔一秒的睡眠进行闲时计数
  1213. await asyncio.sleep(1)
  1214. else:
  1215. await asyncio.sleep(0.1)
  1216. global_idle_time = global_idle_time + 1
  1217. if config.get("idle_time_task", "type") == "直播间无消息更新闲时":
  1218. # 闲时计数达到指定值,进行闲时任务处理
  1219. if global_idle_time >= overflow_time:
  1220. (
  1221. last_mode,
  1222. copywriting_copy_list,
  1223. comment_copy_list,
  1224. local_audio_path_list,
  1225. ) = do_task(
  1226. last_mode,
  1227. copywriting_copy_list,
  1228. comment_copy_list,
  1229. local_audio_path_list,
  1230. )
  1231. elif config.get("idle_time_task", "type") == "待合成消息队列更新闲时":
  1232. if my_handle.is_queue_less_or_greater_than(
  1233. type="message_queue",
  1234. less=int(
  1235. config.get("idle_time_task", "min_msg_queue_len_to_trigger")
  1236. ),
  1237. ):
  1238. (
  1239. last_mode,
  1240. copywriting_copy_list,
  1241. comment_copy_list,
  1242. local_audio_path_list,
  1243. ) = do_task(
  1244. last_mode,
  1245. copywriting_copy_list,
  1246. comment_copy_list,
  1247. local_audio_path_list,
  1248. )
  1249. elif config.get("idle_time_task", "type") == "待播放音频队列更新闲时":
  1250. if my_handle.is_queue_less_or_greater_than(
  1251. type="voice_tmp_path_queue",
  1252. less=int(
  1253. config.get(
  1254. "idle_time_task", "min_audio_queue_len_to_trigger"
  1255. )
  1256. ),
  1257. ):
  1258. (
  1259. last_mode,
  1260. copywriting_copy_list,
  1261. comment_copy_list,
  1262. local_audio_path_list,
  1263. ) = do_task(
  1264. last_mode,
  1265. copywriting_copy_list,
  1266. comment_copy_list,
  1267. local_audio_path_list,
  1268. )
  1269. except Exception as e:
  1270. logger.error(traceback.format_exc())
  1271. if config.get("idle_time_task", "enable"):
  1272. # 创建闲时任务子线程并启动
  1273. threading.Thread(target=lambda: asyncio.run(idle_time_task())).start()
  1274. # 闲时任务计时自动清零
  1275. def idle_time_auto_clear(type: str):
  1276. """闲时任务计时自动清零
  1277. Args:
  1278. type (str): 消息类型(comment/gift/entrance等)
  1279. Returns:
  1280. bool: 是否清零的结果
  1281. """
  1282. global config, global_idle_time
  1283. # 触发的类型列表
  1284. type_list = config.get("idle_time_task", "trigger_type")
  1285. if type in type_list:
  1286. global_idle_time = 0
  1287. return True
  1288. return False
  1289. # 图像识别 定时任务
  1290. def image_recognition_schedule_task(type: str):
  1291. global config, common, my_handle
  1292. logger.debug(f"图像识别-{type} 定时任务执行中...")
  1293. data = {"platform": platform, "username": None, "content": "", "type": type}
  1294. logger.info(f"图像识别-{type} 定时任务触发")
  1295. my_handle.process_data(data, "image_recognition_schedule")
  1296. # 启动图像识别 定时任务
  1297. def run_image_recognition_schedule(interval: int, type: str):
  1298. global config
  1299. try:
  1300. schedule.every(interval).seconds.do(
  1301. partial(image_recognition_schedule_task, type)
  1302. )
  1303. except Exception as e:
  1304. logger.error(traceback.format_exc())
  1305. while True:
  1306. schedule.run_pending()
  1307. # time.sleep(1) # 控制每次循环的间隔时间,避免过多占用 CPU 资源
  1308. if config.get("image_recognition", "loop_screenshot_enable"):
  1309. # 创建定时任务子线程并启动
  1310. image_recognition_schedule_thread = threading.Thread(
  1311. target=lambda: run_image_recognition_schedule(
  1312. config.get("image_recognition", "loop_screenshot_delay"), "窗口截图"
  1313. )
  1314. )
  1315. image_recognition_schedule_thread.start()
  1316. if config.get("image_recognition", "loop_cam_screenshot_enable"):
  1317. # 创建定时任务子线程并启动
  1318. image_recognition_cam_schedule_thread = threading.Thread(
  1319. target=lambda: run_image_recognition_schedule(
  1320. config.get("image_recognition", "loop_cam_screenshot_delay"),
  1321. "摄像头截图",
  1322. )
  1323. )
  1324. image_recognition_cam_schedule_thread.start()
  1325. logger.info(f"当前平台:{platform}")
  1326. if platform == "bilibili":
  1327. from bilibili_api import Credential, live, sync, login
  1328. try:
  1329. if config.get("bilibili", "login_type") == "cookie":
  1330. logger.info(
  1331. "b站登录后F12抓网络包获取cookie,强烈建议使用小号!有封号风险"
  1332. )
  1333. logger.info(
  1334. "b站登录后,F12控制台,输入 window.localStorage.ac_time_value 回车获取(如果没有,请重新登录)"
  1335. )
  1336. bilibili_cookie = config.get("bilibili", "cookie")
  1337. bilibili_ac_time_value = config.get("bilibili", "ac_time_value")
  1338. if bilibili_ac_time_value == "":
  1339. bilibili_ac_time_value = None
  1340. # logger.info(f'SESSDATA={common.parse_cookie_data(bilibili_cookie, "SESSDATA")}')
  1341. # logger.info(f'bili_jct={common.parse_cookie_data(bilibili_cookie, "bili_jct")}')
  1342. # logger.info(f'buvid3={common.parse_cookie_data(bilibili_cookie, "buvid3")}')
  1343. # logger.info(f'DedeUserID={common.parse_cookie_data(bilibili_cookie, "DedeUserID")}')
  1344. # 生成一个 Credential 对象
  1345. credential = Credential(
  1346. sessdata=common.parse_cookie_data(bilibili_cookie, "SESSDATA"),
  1347. bili_jct=common.parse_cookie_data(bilibili_cookie, "bili_jct"),
  1348. buvid3=common.parse_cookie_data(bilibili_cookie, "buvid3"),
  1349. dedeuserid=common.parse_cookie_data(bilibili_cookie, "DedeUserID"),
  1350. ac_time_value=bilibili_ac_time_value,
  1351. )
  1352. elif config.get("bilibili", "login_type") == "手机扫码":
  1353. credential = login.login_with_qrcode()
  1354. elif config.get("bilibili", "login_type") == "手机扫码-终端":
  1355. credential = login.login_with_qrcode_term()
  1356. elif config.get("bilibili", "login_type") == "账号密码登录":
  1357. bilibili_username = config.get("bilibili", "username")
  1358. bilibili_password = config.get("bilibili", "password")
  1359. credential = login.login_with_password(
  1360. bilibili_username, bilibili_password
  1361. )
  1362. elif config.get("bilibili", "login_type") == "不登录":
  1363. credential = None
  1364. else:
  1365. credential = login.login_with_qrcode()
  1366. # 初始化 Bilibili 直播间
  1367. room = live.LiveDanmaku(my_handle.get_room_id(), credential=credential)
  1368. except Exception as e:
  1369. logger.error(traceback.format_exc())
  1370. my_handle.abnormal_alarm_handle("platform")
  1371. # os._exit(0)
  1372. """
  1373. DANMU_MSG: 用户发送弹幕
  1374. SEND_GIFT: 礼物
  1375. COMBO_SEND:礼物连击
  1376. GUARD_BUY:续费大航海
  1377. SUPER_CHAT_MESSAGE:醒目留言(SC)
  1378. SUPER_CHAT_MESSAGE_JPN:醒目留言(带日语翻译?)
  1379. WELCOME: 老爷进入房间
  1380. WELCOME_GUARD: 房管进入房间
  1381. NOTICE_MSG: 系统通知(全频道广播之类的)
  1382. PREPARING: 直播准备中
  1383. LIVE: 直播开始
  1384. ROOM_REAL_TIME_MESSAGE_UPDATE: 粉丝数等更新
  1385. ENTRY_EFFECT: 进场特效
  1386. ROOM_RANK: 房间排名更新
  1387. INTERACT_WORD: 用户进入直播间
  1388. ACTIVITY_BANNER_UPDATE_V2: 好像是房间名旁边那个xx小时榜
  1389. 本模块自定义事件:
  1390. VIEW: 直播间人气更新
  1391. ALL: 所有事件
  1392. DISCONNECT: 断开连接(传入连接状态码参数)
  1393. TIMEOUT: 心跳响应超时
  1394. VERIFICATION_SUCCESSFUL: 认证成功
  1395. """
  1396. @room.on("DANMU_MSG")
  1397. async def _(event):
  1398. """
  1399. 处理直播间弹幕事件
  1400. :param event: 弹幕事件数据
  1401. """
  1402. # 闲时计数清零
  1403. idle_time_auto_clear("comment")
  1404. content = event["data"]["info"][1] # 获取弹幕内容
  1405. username = event["data"]["info"][2][1] # 获取发送弹幕的用户昵称
  1406. logger.info(f"[{username}]: {content}")
  1407. data = {"platform": platform, "username": username, "content": content}
  1408. my_handle.process_data(data, "comment")
  1409. @room.on("COMBO_SEND")
  1410. async def _(event):
  1411. """
  1412. 处理直播间礼物连击事件
  1413. :param event: 礼物连击事件数据
  1414. """
  1415. idle_time_auto_clear("gift")
  1416. gift_name = event["data"]["data"]["gift_name"]
  1417. username = event["data"]["data"]["uname"]
  1418. # 礼物数量
  1419. combo_num = event["data"]["data"]["combo_num"]
  1420. # 总金额
  1421. combo_total_coin = event["data"]["data"]["combo_total_coin"]
  1422. logger.info(
  1423. f"用户:{username} 赠送 {combo_num} 个 {gift_name},总计 {combo_total_coin}电池"
  1424. )
  1425. data = {
  1426. "platform": platform,
  1427. "gift_name": gift_name,
  1428. "username": username,
  1429. "num": combo_num,
  1430. "unit_price": combo_total_coin / combo_num / 1000,
  1431. "total_price": combo_total_coin / 1000,
  1432. }
  1433. my_handle.process_data(data, "gift")
  1434. @room.on("SEND_GIFT")
  1435. async def _(event):
  1436. """
  1437. 处理直播间礼物事件
  1438. :param event: 礼物事件数据
  1439. """
  1440. idle_time_auto_clear("gift")
  1441. # logger.info(event)
  1442. gift_name = event["data"]["data"]["giftName"]
  1443. username = event["data"]["data"]["uname"]
  1444. # 礼物数量
  1445. num = event["data"]["data"]["num"]
  1446. # 总金额
  1447. combo_total_coin = event["data"]["data"]["combo_total_coin"]
  1448. # 单个礼物金额
  1449. discount_price = event["data"]["data"]["discount_price"]
  1450. logger.info(
  1451. f"用户:{username} 赠送 {num} 个 {gift_name},单价 {discount_price}电池,总计 {combo_total_coin}电池"
  1452. )
  1453. data = {
  1454. "platform": platform,
  1455. "gift_name": gift_name,
  1456. "username": username,
  1457. "num": num,
  1458. "unit_price": discount_price / 1000,
  1459. "total_price": combo_total_coin / 1000,
  1460. }
  1461. my_handle.process_data(data, "gift")
  1462. @room.on("GUARD_BUY")
  1463. async def _(event):
  1464. """
  1465. 处理直播间续费大航海事件
  1466. :param event: 续费大航海事件数据
  1467. """
  1468. logger.info(event)
  1469. @room.on("SUPER_CHAT_MESSAGE")
  1470. async def _(event):
  1471. """
  1472. 处理直播间醒目留言(SC)事件
  1473. :param event: 醒目留言(SC)事件数据
  1474. """
  1475. idle_time_auto_clear("gift")
  1476. message = event["data"]["data"]["message"]
  1477. uname = event["data"]["data"]["user_info"]["uname"]
  1478. price = event["data"]["data"]["price"]
  1479. logger.info(f"用户:{uname} 发送 {price}元 SC:{message}")
  1480. data = {
  1481. "platform": platform,
  1482. "gift_name": "SC",
  1483. "username": uname,
  1484. "num": 1,
  1485. "unit_price": price,
  1486. "total_price": price,
  1487. "content": message,
  1488. }
  1489. my_handle.process_data(data, "gift")
  1490. my_handle.process_data(data, "comment")
  1491. @room.on("INTERACT_WORD")
  1492. async def _(event):
  1493. """
  1494. 处理直播间用户进入直播间事件
  1495. :param event: 用户进入直播间事件数据
  1496. """
  1497. global last_username_list
  1498. idle_time_auto_clear("entrance")
  1499. username = event["data"]["data"]["uname"]
  1500. logger.info(f"用户:{username} 进入直播间")
  1501. # 添加用户名到最新的用户名列表
  1502. add_username_to_last_username_list(username)
  1503. data = {"platform": platform, "username": username, "content": "进入直播间"}
  1504. my_handle.process_data(data, "entrance")
  1505. # @room.on('WELCOME')
  1506. # async def _(event):
  1507. # """
  1508. # 处理直播间老爷进入房间事件
  1509. # :param event: 老爷进入房间事件数据
  1510. # """
  1511. # logger.info(event)
  1512. # @room.on('WELCOME_GUARD')
  1513. # async def _(event):
  1514. # """
  1515. # 处理直播间房管进入房间事件
  1516. # :param event: 房管进入房间事件数据
  1517. # """
  1518. # logger.info(event)
  1519. try:
  1520. # 启动 Bilibili 直播间连接
  1521. sync(room.connect())
  1522. except KeyboardInterrupt:
  1523. logger.warning("程序被强行退出")
  1524. finally:
  1525. logger.warning("关闭连接...可能是直播间号配置有误或者其他原因导致的")
  1526. os._exit(0)
  1527. elif platform == "bilibili2":
  1528. import blivedm
  1529. import blivedm.models.web as web_models
  1530. import blivedm.models.open_live as open_models
  1531. global SESSDATA
  1532. # 直播间ID的取值看直播间URL
  1533. TEST_ROOM_IDS = [my_handle.get_room_id()]
  1534. try:
  1535. if config.get("bilibili", "login_type") == "cookie":
  1536. bilibili_cookie = config.get("bilibili", "cookie")
  1537. SESSDATA = common.parse_cookie_data(bilibili_cookie, "SESSDATA")
  1538. elif config.get("bilibili", "login_type") == "open_live":
  1539. # 在开放平台申请的开发者密钥 https://open-live.bilibili.com/open-manage
  1540. ACCESS_KEY_ID = config.get("bilibili", "open_live", "ACCESS_KEY_ID")
  1541. ACCESS_KEY_SECRET = config.get(
  1542. "bilibili", "open_live", "ACCESS_KEY_SECRET"
  1543. )
  1544. # 在开放平台创建的项目ID
  1545. APP_ID = config.get("bilibili", "open_live", "APP_ID")
  1546. # 主播身份码 直播中心获取
  1547. ROOM_OWNER_AUTH_CODE = config.get(
  1548. "bilibili", "open_live", "ROOM_OWNER_AUTH_CODE"
  1549. )
  1550. except Exception as e:
  1551. logger.error(traceback.format_exc())
  1552. my_handle.abnormal_alarm_handle("platform")
  1553. async def main_func():
  1554. global session
  1555. if config.get("bilibili", "login_type") == "open_live":
  1556. await run_single_client2()
  1557. else:
  1558. try:
  1559. init_session()
  1560. await run_single_client()
  1561. await run_multi_clients()
  1562. finally:
  1563. await session.close()
  1564. def init_session():
  1565. global session, SESSDATA
  1566. cookies = http.cookies.SimpleCookie()
  1567. cookies["SESSDATA"] = SESSDATA
  1568. cookies["SESSDATA"]["domain"] = "bilibili.com"
  1569. # logger.info(f"SESSDATA={SESSDATA}")
  1570. # logger.warning(f"sessdata={SESSDATA}")
  1571. # logger.warning(f"cookies={cookies}")
  1572. session = aiohttp.ClientSession()
  1573. session.cookie_jar.update_cookies(cookies)
  1574. async def run_single_client():
  1575. """
  1576. 演示监听一个直播间
  1577. """
  1578. global session
  1579. room_id = random.choice(TEST_ROOM_IDS)
  1580. client = blivedm.BLiveClient(room_id, session=session)
  1581. handler = MyHandler()
  1582. client.set_handler(handler)
  1583. client.start()
  1584. try:
  1585. # 演示5秒后停止
  1586. await asyncio.sleep(5)
  1587. client.stop()
  1588. await client.join()
  1589. finally:
  1590. await client.stop_and_close()
  1591. async def run_single_client2():
  1592. """
  1593. 演示监听一个直播间 开放平台
  1594. """
  1595. client = blivedm.OpenLiveClient(
  1596. access_key_id=ACCESS_KEY_ID,
  1597. access_key_secret=ACCESS_KEY_SECRET,
  1598. app_id=APP_ID,
  1599. room_owner_auth_code=ROOM_OWNER_AUTH_CODE,
  1600. )
  1601. handler = MyHandler2()
  1602. client.set_handler(handler)
  1603. client.start()
  1604. try:
  1605. # 演示70秒后停止
  1606. # await asyncio.sleep(70)
  1607. # client.stop()
  1608. await client.join()
  1609. finally:
  1610. await client.stop_and_close()
  1611. async def run_multi_clients():
  1612. """
  1613. 演示同时监听多个直播间
  1614. """
  1615. global session
  1616. clients = [
  1617. blivedm.BLiveClient(room_id, session=session)
  1618. for room_id in TEST_ROOM_IDS
  1619. ]
  1620. handler = MyHandler()
  1621. for client in clients:
  1622. client.set_handler(handler)
  1623. client.start()
  1624. try:
  1625. await asyncio.gather(*(client.join() for client in clients))
  1626. finally:
  1627. await asyncio.gather(*(client.stop_and_close() for client in clients))
  1628. class MyHandler(blivedm.BaseHandler):
  1629. # 演示如何添加自定义回调
  1630. _CMD_CALLBACK_DICT = blivedm.BaseHandler._CMD_CALLBACK_DICT.copy()
  1631. # 入场消息回调
  1632. def __interact_word_callback(
  1633. self, client: blivedm.BLiveClient, command: dict
  1634. ):
  1635. # logger.info(f"[{client.room_id}] INTERACT_WORD: self_type={type(self).__name__}, room_id={client.room_id},"
  1636. # f" uname={command['data']['uname']}")
  1637. global last_username_list
  1638. idle_time_auto_clear("entrance")
  1639. username = command["data"]["uname"]
  1640. logger.info(f"用户:{username} 进入直播间")
  1641. # 添加用户名到最新的用户名列表
  1642. add_username_to_last_username_list(username)
  1643. data = {
  1644. "platform": platform,
  1645. "username": username,
  1646. "content": "进入直播间",
  1647. }
  1648. my_handle.process_data(data, "entrance")
  1649. _CMD_CALLBACK_DICT["INTERACT_WORD"] = __interact_word_callback # noqa
  1650. def _on_heartbeat(
  1651. self, client: blivedm.BLiveClient, message: web_models.HeartbeatMessage
  1652. ):
  1653. logger.debug(f"[{client.room_id}] 心跳")
  1654. def _on_danmaku(
  1655. self, client: blivedm.BLiveClient, message: web_models.DanmakuMessage
  1656. ):
  1657. # 闲时计数清零
  1658. idle_time_auto_clear("comment")
  1659. # logger.info(f'[{client.room_id}] {message.uname}:{message.msg}')
  1660. content = message.msg # 获取弹幕内容
  1661. username = message.uname # 获取发送弹幕的用户昵称
  1662. # 检查是否存在 face 属性
  1663. user_face = message.face if hasattr(message, "face") else None
  1664. logger.info(f"[{username}]: {content}")
  1665. data = {
  1666. "platform": platform,
  1667. "username": username,
  1668. "user_face": user_face,
  1669. "content": content,
  1670. }
  1671. my_handle.process_data(data, "comment")
  1672. def _on_gift(
  1673. self, client: blivedm.BLiveClient, message: web_models.GiftMessage
  1674. ):
  1675. # logger.info(f'[{client.room_id}] {message.uname} 赠送{message.gift_name}x{message.num}'
  1676. # f' ({message.coin_type}瓜子x{message.total_coin})')
  1677. idle_time_auto_clear("gift")
  1678. gift_name = message.gift_name
  1679. username = message.uname
  1680. # 检查是否存在 face 属性
  1681. user_face = message.face if hasattr(message, "face") else None
  1682. # 礼物数量
  1683. combo_num = message.num
  1684. # 总金额
  1685. combo_total_coin = message.total_coin
  1686. logger.info(
  1687. f"用户:{username} 赠送 {combo_num} 个 {gift_name},总计 {combo_total_coin}电池"
  1688. )
  1689. data = {
  1690. "platform": platform,
  1691. "gift_name": gift_name,
  1692. "username": username,
  1693. "user_face": user_face,
  1694. "num": combo_num,
  1695. "unit_price": combo_total_coin / combo_num / 1000,
  1696. "total_price": combo_total_coin / 1000,
  1697. }
  1698. my_handle.process_data(data, "gift")
  1699. def _on_buy_guard(
  1700. self, client: blivedm.BLiveClient, message: web_models.GuardBuyMessage
  1701. ):
  1702. logger.info(
  1703. f"[{client.room_id}] {message.username} 购买{message.gift_name}"
  1704. )
  1705. def _on_super_chat(
  1706. self, client: blivedm.BLiveClient, message: web_models.SuperChatMessage
  1707. ):
  1708. # logger.info(f'[{client.room_id}] 醒目留言 ¥{message.price} {message.uname}:{message.message}')
  1709. idle_time_auto_clear("gift")
  1710. message = message.message
  1711. uname = message.uname
  1712. # 检查是否存在 face 属性
  1713. user_face = message.face if hasattr(message, "face") else None
  1714. price = message.price
  1715. logger.info(f"用户:{uname} 发送 {price}元 SC:{message}")
  1716. data = {
  1717. "platform": platform,
  1718. "gift_name": "SC",
  1719. "username": uname,
  1720. "user_face": user_face,
  1721. "num": 1,
  1722. "unit_price": price,
  1723. "total_price": price,
  1724. "content": message,
  1725. }
  1726. my_handle.process_data(data, "gift")
  1727. my_handle.process_data(data, "comment")
  1728. class MyHandler2(blivedm.BaseHandler):
  1729. def _on_heartbeat(
  1730. self, client: blivedm.BLiveClient, message: web_models.HeartbeatMessage
  1731. ):
  1732. logger.debug(f"[{client.room_id}] 心跳")
  1733. def _on_open_live_danmaku(
  1734. self,
  1735. client: blivedm.OpenLiveClient,
  1736. message: open_models.DanmakuMessage,
  1737. ):
  1738. # 闲时计数清零
  1739. idle_time_auto_clear("comment")
  1740. # logger.info(f'[{client.room_id}] {message.uname}:{message.msg}')
  1741. content = message.msg # 获取弹幕内容
  1742. username = message.uname # 获取发送弹幕的用户昵称
  1743. # 检查是否存在 face 属性
  1744. user_face = message.face if hasattr(message, "face") else None
  1745. logger.debug(f"用户:{username} 头像:{user_face}")
  1746. logger.info(f"[{username}]: {content}")
  1747. data = {
  1748. "platform": platform,
  1749. "username": username,
  1750. "user_face": user_face,
  1751. "content": content,
  1752. }
  1753. my_handle.process_data(data, "comment")
  1754. def _on_open_live_gift(
  1755. self, client: blivedm.OpenLiveClient, message: open_models.GiftMessage
  1756. ):
  1757. idle_time_auto_clear("gift")
  1758. gift_name = message.gift_name
  1759. username = message.uname
  1760. # 检查是否存在 face 属性
  1761. user_face = message.face if hasattr(message, "face") else None
  1762. # 礼物数量
  1763. combo_num = message.gift_num
  1764. # 总金额
  1765. combo_total_coin = message.price * message.gift_num
  1766. logger.info(
  1767. f"用户:{username} 赠送 {combo_num} 个 {gift_name},总计 {combo_total_coin}电池"
  1768. )
  1769. data = {
  1770. "platform": platform,
  1771. "gift_name": gift_name,
  1772. "username": username,
  1773. "user_face": user_face,
  1774. "num": combo_num,
  1775. "unit_price": combo_total_coin / combo_num / 1000,
  1776. "total_price": combo_total_coin / 1000,
  1777. }
  1778. my_handle.process_data(data, "gift")
  1779. def _on_open_live_buy_guard(
  1780. self,
  1781. client: blivedm.OpenLiveClient,
  1782. message: open_models.GuardBuyMessage,
  1783. ):
  1784. logger.info(
  1785. f"[{client.room_id}] {message.user_info.uname} 购买 大航海等级={message.guard_level}"
  1786. )
  1787. def _on_open_live_super_chat(
  1788. self,
  1789. client: blivedm.OpenLiveClient,
  1790. message: open_models.SuperChatMessage,
  1791. ):
  1792. idle_time_auto_clear("gift")
  1793. logger.info(
  1794. f"[{message.room_id}] 醒目留言 ¥{message.rmb} {message.uname}:{message.message}"
  1795. )
  1796. message = message.message
  1797. uname = message.uname
  1798. # 检查是否存在 face 属性
  1799. user_face = message.face if hasattr(message, "face") else None
  1800. price = message.rmb
  1801. logger.info(f"用户:{uname} 发送 {price}元 SC:{message}")
  1802. data = {
  1803. "platform": platform,
  1804. "gift_name": "SC",
  1805. "username": uname,
  1806. "user_face": user_face,
  1807. "num": 1,
  1808. "unit_price": price,
  1809. "total_price": price,
  1810. "content": message,
  1811. }
  1812. my_handle.process_data(data, "gift")
  1813. my_handle.process_data(data, "comment")
  1814. def _on_open_live_super_chat_delete(
  1815. self,
  1816. client: blivedm.OpenLiveClient,
  1817. message: open_models.SuperChatDeleteMessage,
  1818. ):
  1819. logger.info(
  1820. f"[直播间 {message.room_id}] 删除醒目留言 message_ids={message.message_ids}"
  1821. )
  1822. def _on_open_live_like(
  1823. self, client: blivedm.OpenLiveClient, message: open_models.LikeMessage
  1824. ):
  1825. logger.info(f"用户:{message.uname} 点了个赞")
  1826. asyncio.run(main_func())
  1827. elif platform == "douyu":
  1828. import websockets
  1829. async def on_message(websocket, path):
  1830. global last_liveroom_data, last_username_list
  1831. global global_idle_time
  1832. async for message in websocket:
  1833. # logger.info(f"收到消息: {message}")
  1834. # await websocket.send("服务器收到了你的消息: " + message)
  1835. try:
  1836. data_json = json.loads(message)
  1837. # logger.debug(data_json)
  1838. if data_json["type"] == "comment":
  1839. # logger.info(data_json)
  1840. # 闲时计数清零
  1841. idle_time_auto_clear("comment")
  1842. username = data_json["username"]
  1843. content = data_json["content"]
  1844. logger.info(f"[📧直播间弹幕消息] [{username}]:{content}")
  1845. data = {
  1846. "platform": platform,
  1847. "username": username,
  1848. "content": content,
  1849. }
  1850. my_handle.process_data(data, "comment")
  1851. # 添加用户名到最新的用户名列表
  1852. add_username_to_last_username_list(username)
  1853. except Exception as e:
  1854. logger.error(traceback.format_exc())
  1855. logger.error("数据解析错误!")
  1856. my_handle.abnormal_alarm_handle("platform")
  1857. continue
  1858. async def ws_server():
  1859. ws_url = "127.0.0.1"
  1860. ws_port = 5000
  1861. server = await websockets.serve(on_message, ws_url, ws_port)
  1862. logger.info(f"WebSocket 服务器已在 {ws_url}:{ws_port} 启动")
  1863. await server.wait_closed()
  1864. asyncio.run(ws_server())
  1865. elif platform == "dy":
  1866. import websocket
  1867. def on_message(ws, message):
  1868. global last_liveroom_data, last_username_list, config, config_path
  1869. global global_idle_time
  1870. message_json = json.loads(message)
  1871. # logger.debug(message_json)
  1872. if "Type" in message_json:
  1873. type = message_json["Type"]
  1874. data_json = json.loads(message_json["Data"])
  1875. if type == 1:
  1876. # 闲时计数清零
  1877. idle_time_auto_clear("comment")
  1878. username = data_json["User"]["Nickname"]
  1879. content = data_json["Content"]
  1880. logger.info(f"[📧直播间弹幕消息] [{username}]:{content}")
  1881. data = {
  1882. "platform": platform,
  1883. "username": username,
  1884. "content": content,
  1885. }
  1886. my_handle.process_data(data, "comment")
  1887. pass
  1888. elif type == 2:
  1889. username = data_json["User"]["Nickname"]
  1890. count = data_json["Count"]
  1891. logger.info(f"[👍直播间点赞消息] {username} 点了{count}赞")
  1892. elif type == 3:
  1893. idle_time_auto_clear("entrance")
  1894. username = data_json["User"]["Nickname"]
  1895. logger.info(f"[🚹🚺直播间成员加入消息] 欢迎 {username} 进入直播间")
  1896. data = {
  1897. "platform": platform,
  1898. "username": username,
  1899. "content": "进入直播间",
  1900. }
  1901. # 添加用户名到最新的用户名列表
  1902. add_username_to_last_username_list(username)
  1903. my_handle.process_data(data, "entrance")
  1904. elif type == 4:
  1905. idle_time_auto_clear("follow")
  1906. username = data_json["User"]["Nickname"]
  1907. logger.info(
  1908. f'[➕直播间关注消息] 感谢 {data_json["User"]["Nickname"]} 的关注'
  1909. )
  1910. data = {"platform": platform, "username": username}
  1911. my_handle.process_data(data, "follow")
  1912. pass
  1913. elif type == 5:
  1914. idle_time_auto_clear("gift")
  1915. gift_name = data_json["GiftName"]
  1916. username = data_json["User"]["Nickname"]
  1917. # 礼物数量
  1918. num = data_json["GiftCount"]
  1919. # 礼物重复数量
  1920. repeat_count = data_json["RepeatCount"]
  1921. try:
  1922. # 暂时是写死的
  1923. data_path = "data/抖音礼物价格表.json"
  1924. # 读取JSON文件
  1925. with open(data_path, "r", encoding="utf-8") as file:
  1926. # 解析JSON数据
  1927. data_json = json.load(file)
  1928. if gift_name in data_json:
  1929. # 单个礼物金额 需要自己维护礼物价值表
  1930. discount_price = data_json[gift_name]
  1931. else:
  1932. logger.warning(
  1933. f"数据文件:{data_path} 中,没有 {gift_name} 对应的价值,请手动补充数据"
  1934. )
  1935. discount_price = 1
  1936. except Exception as e:
  1937. logger.error(traceback.format_exc())
  1938. discount_price = 1
  1939. # 总金额
  1940. combo_total_coin = repeat_count * discount_price
  1941. logger.info(
  1942. f"[🎁直播间礼物消息] 用户:{username} 赠送 {num} 个 {gift_name},单价 {discount_price}抖币,总计 {combo_total_coin}抖币"
  1943. )
  1944. data = {
  1945. "platform": platform,
  1946. "gift_name": gift_name,
  1947. "username": username,
  1948. "num": num,
  1949. "unit_price": discount_price / 10,
  1950. "total_price": combo_total_coin / 10,
  1951. }
  1952. my_handle.process_data(data, "gift")
  1953. elif type == 6:
  1954. logger.info(f'[直播间数据] {data_json["Content"]}')
  1955. # {'OnlineUserCount': 50, 'TotalUserCount': 22003, 'TotalUserCountStr': '2.2万', 'OnlineUserCountStr': '50',
  1956. # 'MsgId': 7260517442466662207, 'User': None, 'Content': '当前直播间人数 50,累计直播间人数 2.2万', 'RoomId': 7260415920948906807}
  1957. # logger.info(f"data_json={data_json}")
  1958. last_liveroom_data = data_json
  1959. # 当前在线人数
  1960. OnlineUserCount = data_json["OnlineUserCount"]
  1961. try:
  1962. # 是否开启了动态配置功能
  1963. if config.get("trends_config", "enable"):
  1964. for path_config in config.get("trends_config", "path"):
  1965. online_num_min = int(
  1966. path_config["online_num"].split("-")[0]
  1967. )
  1968. online_num_max = int(
  1969. path_config["online_num"].split("-")[1]
  1970. )
  1971. # 判断在线人数是否在此范围内
  1972. if (
  1973. OnlineUserCount >= online_num_min
  1974. and OnlineUserCount <= online_num_max
  1975. ):
  1976. logger.debug(f"当前配置文件:{path_config['path']}")
  1977. # 如果配置文件相同,则跳过
  1978. if config_path == path_config["path"]:
  1979. break
  1980. config_path = path_config["path"]
  1981. config = Config(config_path)
  1982. my_handle.reload_config(config_path)
  1983. logger.info(f"切换配置文件:{config_path}")
  1984. break
  1985. except Exception as e:
  1986. logger.error(traceback.format_exc())
  1987. pass
  1988. elif type == 8:
  1989. logger.info(
  1990. f'[分享直播间] 感谢 {data_json["User"]["Nickname"]} 分享了直播间'
  1991. )
  1992. pass
  1993. def on_error(ws, error):
  1994. logger.error(f"Error:{error}")
  1995. def on_close(ws):
  1996. logger.debug("WebSocket connection closed")
  1997. def on_open(ws):
  1998. logger.debug("WebSocket connection established")
  1999. try:
  2000. # WebSocket连接URL
  2001. ws_url = "ws://127.0.0.1:8888"
  2002. logger.info(f"监听地址:{ws_url}")
  2003. # 不设置日志等级
  2004. websocket.enableTrace(False)
  2005. # 创建WebSocket连接
  2006. ws = websocket.WebSocketApp(
  2007. ws_url,
  2008. on_message=on_message,
  2009. on_error=on_error,
  2010. on_close=on_close,
  2011. on_open=on_open,
  2012. )
  2013. # 运行WebSocket连接
  2014. ws.run_forever()
  2015. except KeyboardInterrupt:
  2016. logger.warning("程序被强行退出")
  2017. finally:
  2018. logger.warning(
  2019. "关闭ws连接...请确认您是否启动了抖音弹幕监听程序,ws服务正常运行!\n监听程序启动成功后,请重新运行程序进行对接使用!"
  2020. )
  2021. # os._exit(0)
  2022. # 等待子线程结束
  2023. schedule_thread.join()
  2024. elif platform == "dy2":
  2025. # 源自:douyinLiveWebFetcher
  2026. import gzip
  2027. import string
  2028. import requests
  2029. import websocket
  2030. def generateMsToken(length=107):
  2031. """
  2032. 产生请求头部cookie中的msToken字段,其实为随机的107位字符
  2033. :param length:字符位数
  2034. :return:msToken
  2035. """
  2036. random_str = ""
  2037. base_str = string.ascii_letters + string.digits + "=_"
  2038. _len = len(base_str) - 1
  2039. for _ in range(length):
  2040. random_str += base_str[random.randint(0, _len)]
  2041. return random_str
  2042. def generateTtwid():
  2043. """
  2044. 产生请求头部cookie中的ttwid字段,访问抖音网页版直播间首页可以获取到响应cookie中的ttwid
  2045. :return: ttwid
  2046. """
  2047. url = "https://live.douyin.com/"
  2048. headers = {
  2049. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
  2050. "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  2051. }
  2052. try:
  2053. response = requests.get(url, headers=headers)
  2054. response.raise_for_status()
  2055. except Exception as err:
  2056. logger.info("【X】request the live url error: ", err)
  2057. else:
  2058. return response.cookies.get("ttwid")
  2059. class DouyinLiveWebFetcher:
  2060. def __init__(self, live_id):
  2061. """
  2062. 直播间弹幕抓取对象
  2063. :param live_id: 直播间的直播id,打开直播间web首页的链接如:https://live.douyin.com/261378947940,
  2064. 其中的261378947940即是live_id
  2065. """
  2066. self.__ttwid = None
  2067. self.__room_id = None
  2068. self.is_connected = None
  2069. self.live_id = live_id
  2070. self.live_url = "https://live.douyin.com/"
  2071. self.user_agent = (
  2072. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
  2073. "Chrome/120.0.0.0 Safari/537.36"
  2074. )
  2075. def send_heartbeat(self, ws):
  2076. import time, threading
  2077. def heartbeat():
  2078. while True:
  2079. time.sleep(15) # 每15秒发送一次心跳
  2080. if self.is_connected:
  2081. ws.send("hi") # 使用实际的心跳消息格式
  2082. else:
  2083. logger.info("Connection lost, stopping heartbeat.")
  2084. return
  2085. threading.Thread(target=heartbeat).start()
  2086. def start(self):
  2087. self._connectWebSocket()
  2088. def stop(self):
  2089. self.ws.close()
  2090. @property
  2091. def ttwid(self):
  2092. """
  2093. 产生请求头部cookie中的ttwid字段,访问抖音网页版直播间首页可以获取到响应cookie中的ttwid
  2094. :return: ttwid
  2095. """
  2096. if self.__ttwid:
  2097. return self.__ttwid
  2098. headers = {
  2099. "User-Agent": self.user_agent,
  2100. }
  2101. try:
  2102. response = requests.get(self.live_url, headers=headers)
  2103. response.raise_for_status()
  2104. except Exception as err:
  2105. logger.info("【X】Request the live url error: ", err)
  2106. else:
  2107. self.__ttwid = response.cookies.get("ttwid")
  2108. return self.__ttwid
  2109. @property
  2110. def room_id(self):
  2111. """
  2112. 根据直播间的地址获取到真正的直播间roomId,有时会有错误,可以重试请求解决
  2113. :return:room_id
  2114. """
  2115. if self.__room_id:
  2116. return self.__room_id
  2117. url = self.live_url + self.live_id
  2118. headers = {
  2119. "User-Agent": self.user_agent,
  2120. "cookie": f"ttwid={self.ttwid}&msToken={generateMsToken()}; __ac_nonce=0123407cc00a9e438deb4",
  2121. }
  2122. try:
  2123. response = requests.get(url, headers=headers)
  2124. response.raise_for_status()
  2125. except Exception as err:
  2126. logger.error("【X】Request the live room url error: ", err)
  2127. return None
  2128. else:
  2129. match = re.search(r'roomId\\":\\"(\d+)\\"', response.text)
  2130. if match is None or len(match.groups()) < 1:
  2131. logger.error(
  2132. "【X】无法获取 真 roomId,可能是直播间号配置错了,或者被官方拉黑了"
  2133. )
  2134. return None
  2135. self.__room_id = match.group(1)
  2136. return self.__room_id
  2137. def _connectWebSocket(self):
  2138. """
  2139. 连接抖音直播间websocket服务器,请求直播间数据
  2140. """
  2141. wss = (
  2142. f"wss://webcast3-ws-web-lq.douyin.com/webcast/im/push/v2/?"
  2143. f"app_name=douyin_web&version_code=180800&webcast_sdk_version=1.3.0&update_version_code=1.3.0"
  2144. f"&compress=gzip"
  2145. f"&internal_ext=internal_src:dim|wss_push_room_id:{self.room_id}|wss_push_did:{self.room_id}"
  2146. f"|dim_log_id:202302171547011A160A7BAA76660E13ED|fetch_time:1676620021641|seq:1|wss_info:0-1676"
  2147. f"620021641-0-0|wrds_kvs:WebcastRoomStatsMessage-1676620020691146024_WebcastRoomRankMessage-167661"
  2148. f"9972726895075_AudienceGiftSyncData-1676619980834317696_HighlightContainerSyncData-2&cursor=t-1676"
  2149. f"620021641_r-1_d-1_u-1_h-1"
  2150. f"&host=https://live.douyin.com&aid=6383&live_id=1"
  2151. f"&did_rule=3&debug=false&endpoint=live_pc&support_wrds=1&"
  2152. f"im_path=/webcast/im/fetch/&user_unique_id={self.room_id}&"
  2153. f"device_platform=web&cookie_enabled=true&screen_width=1440&screen_height=900&browser_language=zh&"
  2154. f"browser_platform=MacIntel&browser_name=Mozilla&"
  2155. f"browser_version=5.0%20(Macintosh;%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit/537.36%20(KHTML,%20"
  2156. f"like%20Gecko)%20Chrome/110.0.0.0%20Safari/537.36&"
  2157. f"browser_online=true&tz_name=Asia/Shanghai&identity=audience&"
  2158. f"room_id={self.room_id}&heartbeatDuration=0&signature=00000000"
  2159. )
  2160. # 直接从直播间抓包ws,赋值url地址填这,在被官方拉黑的情况下用
  2161. # wss = "wss://webcast5-ws-web-lq.douyin.com/webcast/im/push/v2/?app_name=douyin_web&version_code=180800&webcast_sdk_version=1.0.14-beta.0&update_version_code=1.0.14-beta.0&compress=gzip&device_platform=web&cookie_enabled=true&screen_width=2048&screen_height=1152&browser_language=zh-CN&browser_platform=Win32&browser_name=Mozilla&browser_version=5.0%20(Windows%20NT%2010.0;%20Win64;%20x64)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/126.0.0.0%20Safari/537.36%20Edg/126.0.0.0&browser_online=true&tz_name=Etc/GMT-8&cursor=h-7383323426352862262_t-1719063974519_r-1_d-1_u-1&internal_ext=internal_src:dim|wss_push_room_id:7383264938631973686|wss_push_did:7293153952199050788|first_req_ms:1719063974385|fetch_time:1719063974519|seq:1|wss_info:0-1719063974519-0-0|wrds_v:7383323492227230262&host=https://live.douyin.com&aid=6383&live_id=1&did_rule=3&endpoint=live_pc&support_wrds=1&user_unique_id=7293153952199050788&im_path=/webcast/im/fetch/&identity=audience&need_persist_msg_count=15&insert_task_id=&live_reason=&room_id=7383264938631973686&heartbeatDuration=0&signature=6DJMtCOOuubiYZP4"
  2162. headers = {
  2163. "cookie": f"ttwid={self.ttwid}",
  2164. "user-agent": self.user_agent,
  2165. }
  2166. self.ws = websocket.WebSocketApp(
  2167. wss,
  2168. header=headers,
  2169. on_open=self._wsOnOpen,
  2170. on_message=self._wsOnMessage,
  2171. on_error=self._wsOnError,
  2172. on_close=self._wsOnClose,
  2173. )
  2174. try:
  2175. self.ws.run_forever()
  2176. except Exception:
  2177. self.stop()
  2178. raise
  2179. def _wsOnOpen(self, ws):
  2180. """
  2181. 连接建立成功
  2182. """
  2183. logger.info("WebSocket connected.")
  2184. self.is_connected = True
  2185. def _wsOnMessage(self, ws, message):
  2186. """
  2187. 接收到数据
  2188. :param ws: websocket实例
  2189. :param message: 数据
  2190. """
  2191. # 根据proto结构体解析对象
  2192. package = PushFrame().parse(message)
  2193. response = Response().parse(gzip.decompress(package.payload))
  2194. # 返回直播间服务器链接存活确认消息,便于持续获取数据
  2195. if response.need_ack:
  2196. ack = PushFrame(
  2197. log_id=package.log_id,
  2198. payload_type="ack",
  2199. payload=response.internal_ext.encode("utf-8"),
  2200. ).SerializeToString()
  2201. ws.send(ack, websocket.ABNF.OPCODE_BINARY)
  2202. # 根据消息类别解析消息体
  2203. for msg in response.messages_list:
  2204. method = msg.method
  2205. try:
  2206. {
  2207. "WebcastChatMessage": self._parseChatMsg, # 聊天消息
  2208. "WebcastGiftMessage": self._parseGiftMsg, # 礼物消息
  2209. "WebcastLikeMessage": self._parseLikeMsg, # 点赞消息
  2210. "WebcastMemberMessage": self._parseMemberMsg, # 进入直播间消息
  2211. "WebcastSocialMessage": self._parseSocialMsg, # 关注消息
  2212. "WebcastRoomUserSeqMessage": self._parseRoomUserSeqMsg, # 直播间统计
  2213. "WebcastFansclubMessage": self._parseFansclubMsg, # 粉丝团消息
  2214. "WebcastControlMessage": self._parseControlMsg, # 直播间状态消息
  2215. "WebcastEmojiChatMessage": self._parseEmojiChatMsg, # 聊天表情包消息
  2216. "WebcastRoomStatsMessage": self._parseRoomStatsMsg, # 直播间统计信息
  2217. "WebcastRoomMessage": self._parseRoomMsg, # 直播间信息
  2218. "WebcastRoomRankMessage": self._parseRankMsg, # 直播间排行榜信息
  2219. }.get(method)(msg.payload)
  2220. except Exception:
  2221. pass
  2222. def _wsOnError(self, ws, error):
  2223. logger.info("WebSocket error: ", error)
  2224. self.is_connected = False
  2225. def _wsOnClose(self, ws):
  2226. logger.info("WebSocket connection closed.")
  2227. self.is_connected = False
  2228. def _parseChatMsg(self, payload):
  2229. """聊天消息"""
  2230. message = ChatMessage().parse(payload)
  2231. username = message.user.nick_name
  2232. user_id = message.user.id
  2233. content = message.content
  2234. logger.info(f"【聊天msg】[{user_id}]{username}: {content}")
  2235. data = {"platform": platform, "username": username, "content": content}
  2236. my_handle.process_data(data, "comment")
  2237. def _parseGiftMsg(self, payload):
  2238. """礼物消息"""
  2239. message = GiftMessage().parse(payload)
  2240. username = message.user.nick_name
  2241. gift_name = message.gift.name
  2242. num = message.combo_count
  2243. logger.info(f"【礼物msg】{username} 送出了 {gift_name}x{num}")
  2244. try:
  2245. # 暂时是写死的
  2246. data_path = "data/抖音礼物价格表.json"
  2247. # 读取JSON文件
  2248. with open(data_path, "r", encoding="utf-8") as file:
  2249. # 解析JSON数据
  2250. data_json = json.load(file)
  2251. if gift_name in data_json:
  2252. # 单个礼物金额 需要自己维护礼物价值表
  2253. discount_price = data_json[gift_name]
  2254. else:
  2255. logger.warning(
  2256. f"数据文件:{data_path} 中,没有 {gift_name} 对应的价值,请手动补充数据"
  2257. )
  2258. discount_price = 1
  2259. except Exception as e:
  2260. logger.error(traceback.format_exc())
  2261. discount_price = 1
  2262. # 总金额
  2263. combo_total_coin = num * discount_price
  2264. data = {
  2265. "platform": platform,
  2266. "gift_name": gift_name,
  2267. "username": username,
  2268. "num": num,
  2269. "unit_price": discount_price / 10,
  2270. "total_price": combo_total_coin / 10,
  2271. }
  2272. my_handle.process_data(data, "gift")
  2273. def _parseLikeMsg(self, payload):
  2274. """点赞消息"""
  2275. message = LikeMessage().parse(payload)
  2276. user_name = message.user.nick_name
  2277. count = message.count
  2278. logger.info(f"【点赞msg】{user_name} 点了{count}个赞")
  2279. def _parseMemberMsg(self, payload):
  2280. """进入直播间消息"""
  2281. message = MemberMessage().parse(payload)
  2282. username = message.user.nick_name
  2283. user_id = message.user.id
  2284. gender = ["女", "男"][message.user.gender]
  2285. logger.info(f"【进场msg】[{user_id}][{gender}]{username} 进入了直播间")
  2286. data = {
  2287. "platform": platform,
  2288. "username": username,
  2289. "content": "进入直播间",
  2290. }
  2291. # 添加用户名到最新的用户名列表
  2292. add_username_to_last_username_list(username)
  2293. my_handle.process_data(data, "entrance")
  2294. def _parseSocialMsg(self, payload):
  2295. """关注消息"""
  2296. message = SocialMessage().parse(payload)
  2297. user_name = message.user.nick_name
  2298. user_id = message.user.id
  2299. logger.info(f"【关注msg】[{user_id}]{user_name} 关注了主播")
  2300. data = {"platform": platform, "username": username}
  2301. my_handle.process_data(data, "follow")
  2302. def _parseRoomUserSeqMsg(self, payload):
  2303. """直播间统计"""
  2304. message = RoomUserSeqMessage().parse(payload)
  2305. OnlineUserCount = message.total
  2306. total = message.total_pv_for_anchor
  2307. logger.info(
  2308. f"【统计msg】当前观看人数: {OnlineUserCount}, 累计观看人数: {total}"
  2309. )
  2310. try:
  2311. global last_liveroom_data
  2312. # {'OnlineUserCount': 50, 'TotalUserCount': 22003, 'TotalUserCountStr': '2.2万', 'OnlineUserCountStr': '50',
  2313. # 'MsgId': 7260517442466662207, 'User': None, 'Content': '当前直播间人数 50,累计直播间人数 2.2万', 'RoomId': 7260415920948906807}
  2314. # logger.info(f"data_json={data_json}")
  2315. last_liveroom_data = {
  2316. "OnlineUserCount": OnlineUserCount,
  2317. "TotalUserCountStr": total,
  2318. }
  2319. # 是否开启了动态配置功能
  2320. if config.get("trends_config", "enable"):
  2321. for path_config in config.get("trends_config", "path"):
  2322. online_num_min = int(
  2323. path_config["online_num"].split("-")[0]
  2324. )
  2325. online_num_max = int(
  2326. path_config["online_num"].split("-")[1]
  2327. )
  2328. # 判断在线人数是否在此范围内
  2329. if (
  2330. OnlineUserCount >= online_num_min
  2331. and OnlineUserCount <= online_num_max
  2332. ):
  2333. logger.debug(f"当前配置文件:{path_config['path']}")
  2334. # 如果配置文件相同,则跳过
  2335. if config_path == path_config["path"]:
  2336. break
  2337. config_path = path_config["path"]
  2338. config = Config(config_path)
  2339. my_handle.reload_config(config_path)
  2340. logger.info(f"切换配置文件:{config_path}")
  2341. break
  2342. except Exception as e:
  2343. logger.error(traceback.format_exc())
  2344. pass
  2345. def _parseFansclubMsg(self, payload):
  2346. """粉丝团消息"""
  2347. message = FansclubMessage().parse(payload)
  2348. content = message.content
  2349. logger.info(f"【粉丝团msg】 {content}")
  2350. def _parseEmojiChatMsg(self, payload):
  2351. """聊天表情包消息"""
  2352. message = EmojiChatMessage().parse(payload)
  2353. emoji_id = message.emoji_id
  2354. user = message.user
  2355. common = message.common
  2356. default_content = message.default_content
  2357. logger.info(
  2358. f"【聊天表情包id】 {emoji_id},user:{user},common:{common},default_content:{default_content}"
  2359. )
  2360. def _parseRoomMsg(self, payload):
  2361. message = RoomMessage().parse(payload)
  2362. common = message.common
  2363. room_id = common.room_id
  2364. logger.info(f"【直播间msg】直播间id:{room_id}")
  2365. def _parseRoomStatsMsg(self, payload):
  2366. message = RoomStatsMessage().parse(payload)
  2367. display_long = message.display_long
  2368. logger.info(f"【直播间统计msg】{display_long}")
  2369. def _parseRankMsg(self, payload):
  2370. message = RoomRankMessage().parse(payload)
  2371. ranks_list = message.ranks_list
  2372. logger.info(f"【直播间排行榜msg】{ranks_list}")
  2373. def _parseControlMsg(self, payload):
  2374. """直播间状态消息"""
  2375. message = ControlMessage().parse(payload)
  2376. if message.status == 3:
  2377. logger.info("直播间已结束")
  2378. self.stop()
  2379. config_room_id = my_handle.get_room_id()
  2380. DouyinLiveWebFetcher(config_room_id).start()
  2381. elif platform == "ks2":
  2382. import websockets
  2383. async def on_message(websocket, path):
  2384. global last_liveroom_data, last_username_list
  2385. global global_idle_time
  2386. async for message in websocket:
  2387. # logger.info(f"收到消息: {message}")
  2388. # await websocket.send("服务器收到了你的消息: " + message)
  2389. try:
  2390. data_json = json.loads(message)
  2391. # logger.debug(data_json)
  2392. if data_json["type"] == "comment":
  2393. # logger.info(data_json)
  2394. # 闲时计数清零
  2395. idle_time_auto_clear("comment")
  2396. username = data_json["username"]
  2397. content = data_json["content"]
  2398. logger.info(f"[📧直播间弹幕消息] [{username}]:{content}")
  2399. data = {
  2400. "platform": platform,
  2401. "username": username,
  2402. "content": content,
  2403. }
  2404. my_handle.process_data(data, "comment")
  2405. # 添加用户名到最新的用户名列表
  2406. add_username_to_last_username_list(username)
  2407. except Exception as e:
  2408. logger.error(traceback.format_exc())
  2409. logger.error("数据解析错误!")
  2410. my_handle.abnormal_alarm_handle("platform")
  2411. continue
  2412. async def ws_server():
  2413. ws_url = "127.0.0.1"
  2414. ws_port = 5000
  2415. server = await websockets.serve(on_message, ws_url, ws_port)
  2416. logger.info(f"WebSocket 服务器已在 {ws_url}:{ws_port} 启动")
  2417. await server.wait_closed()
  2418. asyncio.run(ws_server())
  2419. elif platform == "ks":
  2420. from playwright.sync_api import sync_playwright, TimeoutError
  2421. from google.protobuf.json_format import MessageToDict
  2422. from configparser import ConfigParser
  2423. import kuaishou_pb2
  2424. class kslive(object):
  2425. def __init__(self):
  2426. global config, common, my_handle
  2427. self.path = os.path.abspath("")
  2428. self.chrome_path = r"\firefox-1419\firefox\firefox.exe"
  2429. self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0"
  2430. self.uri = "https://live.kuaishou.com/u/"
  2431. self.context = None
  2432. self.browser = None
  2433. self.page = None
  2434. try:
  2435. self.live_ids = config.get("room_display_id")
  2436. self.thread = 2
  2437. # 没什么用的手机号配置,也就方便登录
  2438. self.phone = "123"
  2439. except Exception as e:
  2440. logger.error(traceback.format_exc())
  2441. logger.error("请检查配置文件")
  2442. my_handle.abnormal_alarm_handle("platform")
  2443. exit()
  2444. def find_file(self, find_path, file_type) -> list:
  2445. """
  2446. 寻找文件
  2447. :param find_path: 子路径
  2448. :param file_type: 文件类型
  2449. :return:
  2450. """
  2451. path = self.path + "\\" + find_path
  2452. data_list = []
  2453. for root, dirs, files in os.walk(path):
  2454. if root != path:
  2455. break
  2456. for file in files:
  2457. file_path = os.path.join(root, file)
  2458. if file_path.find(file_type) != -1:
  2459. data_list.append(file_path)
  2460. return data_list
  2461. def main(self, lid, semaphore):
  2462. if not os.path.exists(self.path + "\\cookie"):
  2463. os.makedirs(self.path + "\\cookie")
  2464. cookie_path = self.path + "\\cookie\\" + self.phone + ".json"
  2465. # if not os.path.exists(cookie_path):
  2466. # with open(cookie_path, 'w') as file:
  2467. # file.write('{"a":"a"}')
  2468. # logger.info(f"'{cookie_path}' 创建成功")
  2469. # else:
  2470. # logger.info(f"'{cookie_path}' 已存在,无需创建")
  2471. with semaphore:
  2472. thread_name = threading.current_thread().name.split("-")[0]
  2473. with sync_playwright() as p:
  2474. self.browser = p.chromium.launch(headless=False)
  2475. # self.browser = p.firefox.launch(headless=False)
  2476. # executable_path=self.path + self.chrome_path
  2477. cookie_list = self.find_file("cookie", "json")
  2478. live_url = self.uri + lid
  2479. if not os.path.exists(cookie_path):
  2480. self.context = self.browser.new_context(
  2481. storage_state=None, user_agent=self.ua
  2482. )
  2483. else:
  2484. self.context = self.browser.new_context(
  2485. storage_state=cookie_list[0], user_agent=self.ua
  2486. )
  2487. self.page = self.context.new_page()
  2488. self.page.add_init_script(
  2489. "Object.defineProperties(navigator, {webdriver:{get:()=>undefined}});"
  2490. )
  2491. self.page.goto("https://live.kuaishou.com/")
  2492. # self.page.goto(live_url)
  2493. element = self.page.get_attribute(".no-login", "style")
  2494. if not element:
  2495. logger.info("未登录,请先登录~")
  2496. self.page.locator(".login").click()
  2497. self.page.locator(
  2498. "li.tab-panel:nth-child(2) > h4:nth-child(1)"
  2499. ).click()
  2500. self.page.locator(
  2501. "div.normal-login-item:nth-child(1) > div:nth-child(1) > input:nth-child(1)"
  2502. ).fill(self.phone)
  2503. try:
  2504. self.page.wait_for_selector(
  2505. "#app > section > div.header-placeholder > header > div.header-main > "
  2506. "div.right-part > div.user-info > div.tooltip-trigger > span",
  2507. timeout=1000 * 60 * 2,
  2508. )
  2509. if not os.path.exists(self.path + "\\cookie"):
  2510. os.makedirs(self.path + "\\cookie")
  2511. self.context.storage_state(path=cookie_path)
  2512. # 检测是否开播
  2513. selector = (
  2514. "html body div#app div.live-room div.detail div.player "
  2515. "div.kwai-player.kwai-player-container.kwai-player-rotation-0 "
  2516. "div.kwai-player-container-video div.kwai-player-plugins div.center-state div.state "
  2517. "div.no-live-detail div.desc p.tip"
  2518. ) # 检测正在直播时下播的选择器
  2519. try:
  2520. msg = self.page.locator(selector).text_content(
  2521. timeout=3000
  2522. )
  2523. logger.info("当前%s" % thread_name + "," + msg)
  2524. self.context.close()
  2525. self.browser.close()
  2526. except Exception as e:
  2527. logger.info("当前%s,[%s]正在直播" % (thread_name, lid))
  2528. logger.info(f"跳转直播间:{live_url}")
  2529. # self.page.goto(live_url)
  2530. # time.sleep(1)
  2531. self.page.goto(live_url)
  2532. # 等待一段时间检查是否有验证码弹窗
  2533. try:
  2534. captcha_selector = "html body div.container" # 假设这是验证码弹窗的选择器
  2535. self.page.wait_for_selector(
  2536. captcha_selector, timeout=5000
  2537. ) # 等待5秒看是否出现验证码
  2538. logger.info("检测到验证码,处理验证码...")
  2539. # 等待验证码弹窗从DOM中被完全移除
  2540. self.page.wait_for_selector(
  2541. captcha_selector,
  2542. state="detached",
  2543. timeout=10000,
  2544. ) # 假设最长等待10秒验证码验证完成
  2545. logger.info("验证码已验证,弹窗已移除")
  2546. # 弹窗处理逻辑之后等待1秒
  2547. time.sleep(1)
  2548. # 处理完验证码后,可能需要再次跳转页面
  2549. # self.page.goto(live_url)
  2550. except TimeoutError:
  2551. logger.error("没有检测到验证码,继续执行...")
  2552. logger.info(f"请在10s内手动打开直播间:{live_url}")
  2553. time.sleep(10)
  2554. self.page.on("websocket", self.web_sockets)
  2555. logger.info(f"24h监听直播间等待下播...")
  2556. self.page.wait_for_selector(selector, timeout=86400000)
  2557. logger.error(
  2558. "当前%s,[%s]的直播结束了" % (thread_name, lid)
  2559. )
  2560. self.context.close()
  2561. self.browser.close()
  2562. except Exception as e:
  2563. logger.error(traceback.format_exc())
  2564. self.context.close()
  2565. self.browser.close()
  2566. def web_sockets(self, web_socket):
  2567. logger.info("web_sockets...")
  2568. urls = web_socket.url
  2569. logger.info(urls)
  2570. if "/websocket" in urls:
  2571. logger.info("websocket连接成功,创建监听事件")
  2572. web_socket.on("close", self.websocket_close)
  2573. web_socket.on("framereceived", self.handler)
  2574. def websocket_close(self):
  2575. self.context.close()
  2576. self.browser.close()
  2577. def handler(self, websocket):
  2578. Message = kuaishou_pb2.SocketMessage()
  2579. Message.ParseFromString(websocket)
  2580. if Message.payloadType == 310:
  2581. SCWebFeedPUsh = kuaishou_pb2.SCWebFeedPush()
  2582. SCWebFeedPUsh.ParseFromString(Message.payload)
  2583. obj = MessageToDict(SCWebFeedPUsh, preserving_proto_field_name=True)
  2584. logger.debug(obj)
  2585. if obj.get("commentFeeds", ""):
  2586. msg_list = obj.get("commentFeeds", "")
  2587. for i in msg_list:
  2588. # 闲时计数清零
  2589. idle_time_auto_clear("comment")
  2590. username = i["user"]["userName"]
  2591. pid = i["user"]["principalId"]
  2592. content = i["content"]
  2593. logger.info(f"[📧直播间弹幕消息] [{username}]:{content}")
  2594. data = {
  2595. "platform": platform,
  2596. "username": username,
  2597. "content": content,
  2598. }
  2599. my_handle.process_data(data, "comment")
  2600. if obj.get("giftFeeds", ""):
  2601. idle_time_auto_clear("gift")
  2602. msg_list = obj.get("giftFeeds", "")
  2603. for i in msg_list:
  2604. username = i["user"]["userName"]
  2605. # pid = i['user']['principalId']
  2606. giftId = i["giftId"]
  2607. comboCount = i["comboCount"]
  2608. logger.info(
  2609. f"[🎁直播间礼物消息] 用户:{username} 赠送礼物Id={giftId} 连击数={comboCount}"
  2610. )
  2611. if obj.get("likeFeeds", ""):
  2612. msg_list = obj.get("likeFeeds", "")
  2613. for i in msg_list:
  2614. username = i["user"]["userName"]
  2615. pid = i["user"]["principalId"]
  2616. logger.info(f"{username}")
  2617. class run(kslive):
  2618. def __init__(self):
  2619. super().__init__()
  2620. self.ids_list = self.live_ids.split(",")
  2621. def run_live(self):
  2622. """
  2623. 主程序入口
  2624. :return:
  2625. """
  2626. t_list = []
  2627. # 允许的最大线程数
  2628. if self.thread < 1:
  2629. self.thread = 1
  2630. elif self.thread > 8:
  2631. self.thread = 8
  2632. logger.info("线程最大允许8,线程数最好设置cpu核心数")
  2633. semaphore = threading.Semaphore(self.thread)
  2634. # 用于记录数量
  2635. n = 0
  2636. if not self.live_ids:
  2637. logger.info("请导入网页直播id,多个以','间隔")
  2638. return
  2639. for i in self.ids_list:
  2640. n += 1
  2641. t = threading.Thread(
  2642. target=kslive().main, args=(i, semaphore), name=f"线程:{n}-{i}"
  2643. )
  2644. t.start()
  2645. t_list.append(t)
  2646. for i in t_list:
  2647. i.join()
  2648. run().run_live()
  2649. elif platform in ["pdd", "1688"]:
  2650. import websockets
  2651. async def on_message(websocket, path):
  2652. global last_liveroom_data, last_username_list
  2653. global global_idle_time
  2654. async for message in websocket:
  2655. # logger.info(f"收到消息: {message}")
  2656. # await websocket.send("服务器收到了你的消息: " + message)
  2657. try:
  2658. data_json = json.loads(message)
  2659. # logger.debug(data_json)
  2660. if data_json["type"] == "comment":
  2661. # logger.info(data_json)
  2662. # 闲时计数清零
  2663. idle_time_auto_clear("comment")
  2664. username = data_json["username"]
  2665. content = data_json["content"]
  2666. logger.info(f"[📧直播间弹幕消息] [{username}]:{content}")
  2667. data = {
  2668. "platform": platform,
  2669. "username": username,
  2670. "content": content,
  2671. }
  2672. my_handle.process_data(data, "comment")
  2673. # 添加用户名到最新的用户名列表
  2674. add_username_to_last_username_list(username)
  2675. except Exception as e:
  2676. logger.error(traceback.format_exc())
  2677. logger.error("数据解析错误!")
  2678. my_handle.abnormal_alarm_handle("platform")
  2679. continue
  2680. async def ws_server():
  2681. ws_url = "127.0.0.1"
  2682. ws_port = 5000
  2683. server = await websockets.serve(on_message, ws_url, ws_port)
  2684. logger.info(f"WebSocket 服务器已在 {ws_url}:{ws_port} 启动")
  2685. await server.wait_closed()
  2686. asyncio.run(ws_server())
  2687. elif platform == "tiktok":
  2688. """
  2689. tiktok
  2690. """
  2691. from TikTokLive import TikTokLiveClient
  2692. from TikTokLive.events import (
  2693. CommentEvent,
  2694. ConnectEvent,
  2695. DisconnectEvent,
  2696. JoinEvent,
  2697. GiftEvent,
  2698. FollowEvent,
  2699. )
  2700. # from TikTokLive.client.errors import LiveNotFound
  2701. # 比如直播间是 https://www.tiktok.com/@username/live 那么room_id就是 username,其实就是用户唯一ID
  2702. room_id = my_handle.get_room_id()
  2703. proxys = {
  2704. "http://": "http://127.0.0.1:10809",
  2705. "https://": "http://127.0.0.1:10809",
  2706. }
  2707. proxys = None
  2708. # 代理软件开启TUN模式进行代理,由于库的ws不走传入的代理参数,只能靠代理软件全代理了
  2709. client: TikTokLiveClient = TikTokLiveClient(
  2710. unique_id=f"@{room_id}", web_proxy=proxys, ws_proxy=proxys
  2711. )
  2712. def start_client():
  2713. # Define how you want to handle specific events via decorator
  2714. @client.on("connect")
  2715. async def on_connect(_: ConnectEvent):
  2716. logger.info(f"连接到 房间ID:{client.room_id}")
  2717. @client.on("disconnect")
  2718. async def on_disconnect(event: DisconnectEvent):
  2719. logger.info("断开连接,10秒后重连")
  2720. await asyncio.sleep(10) # 等待一段时间后尝试重连,这里等待10秒
  2721. start_client() # 尝试重新连接
  2722. @client.on("join")
  2723. async def on_join(event: JoinEvent):
  2724. idle_time_auto_clear("entrance")
  2725. username = event.user.nickname
  2726. unique_id = event.user.unique_id
  2727. logger.info(f"[🚹🚺直播间成员加入消息] 欢迎 {username} 进入直播间")
  2728. data = {
  2729. "platform": platform,
  2730. "username": username,
  2731. "content": "进入直播间",
  2732. }
  2733. # 添加用户名到最新的用户名列表
  2734. add_username_to_last_username_list(username)
  2735. my_handle.process_data(data, "entrance")
  2736. # Notice no decorator?
  2737. @client.on("comment")
  2738. async def on_comment(event: CommentEvent):
  2739. # 闲时计数清零
  2740. idle_time_auto_clear("comment")
  2741. username = event.user.nickname
  2742. content = event.comment
  2743. logger.info(f"[📧直播间弹幕消息] [{username}]:{content}")
  2744. data = {"platform": platform, "username": username, "content": content}
  2745. my_handle.process_data(data, "comment")
  2746. @client.on("gift")
  2747. async def on_gift(event: GiftEvent):
  2748. """
  2749. This is an example for the "gift" event to show you how to read gift data properly.
  2750. Important Note:
  2751. Gifts of type 1 can have streaks, so we need to check that the streak has ended
  2752. If the gift type isn't 1, it can't repeat. Therefore, we can go straight to logger.infoing
  2753. """
  2754. idle_time_auto_clear("gift")
  2755. # Streakable gift & streak is over
  2756. if event.gift.streakable and not event.gift.streaking:
  2757. # 礼物重复数量
  2758. repeat_count = event.gift.count
  2759. # Non-streakable gift
  2760. elif not event.gift.streakable:
  2761. # 礼物重复数量
  2762. repeat_count = 1
  2763. gift_name = event.gift.info.name
  2764. username = event.user.nickname
  2765. # 礼物数量
  2766. num = 1
  2767. try:
  2768. # 暂时是写死的
  2769. data_path = "data/tiktok礼物价格表.json"
  2770. # 读取JSON文件
  2771. with open(data_path, "r", encoding="utf-8") as file:
  2772. # 解析JSON数据
  2773. data_json = json.load(file)
  2774. if gift_name in data_json:
  2775. # 单个礼物金额 需要自己维护礼物价值表
  2776. discount_price = data_json[gift_name]
  2777. else:
  2778. logger.warning(
  2779. f"数据文件:{data_path} 中,没有 {gift_name} 对应的价值,请手动补充数据"
  2780. )
  2781. discount_price = 1
  2782. except Exception as e:
  2783. logger.error(traceback.format_exc())
  2784. discount_price = 1
  2785. # 总金额
  2786. combo_total_coin = repeat_count * discount_price
  2787. logger.info(
  2788. f"[🎁直播间礼物消息] 用户:{username} 赠送 {num} 个 {gift_name},单价 {discount_price}抖币,总计 {combo_total_coin}抖币"
  2789. )
  2790. data = {
  2791. "platform": platform,
  2792. "gift_name": gift_name,
  2793. "username": username,
  2794. "num": num,
  2795. "unit_price": discount_price / 10,
  2796. "total_price": combo_total_coin / 10,
  2797. }
  2798. my_handle.process_data(data, "gift")
  2799. @client.on("follow")
  2800. async def on_follow(event: FollowEvent):
  2801. idle_time_auto_clear("follow")
  2802. username = event.user.nickname
  2803. logger.info(f"[➕直播间关注消息] 感谢 {username} 的关注")
  2804. data = {"platform": platform, "username": username}
  2805. my_handle.process_data(data, "follow")
  2806. try:
  2807. client.stop()
  2808. logger.info(f"连接{room_id}中...")
  2809. client.run()
  2810. except Exception as e:
  2811. logger.info(f"用户ID: @{client.unique_id} 好像不在线捏, 1分钟后重试...")
  2812. start_client()
  2813. # 运行客户端
  2814. start_client()
  2815. elif platform == "twitch":
  2816. import socks
  2817. from emoji import demojize
  2818. try:
  2819. server = "irc.chat.twitch.tv"
  2820. port = 6667
  2821. nickname = "主人"
  2822. try:
  2823. channel = (
  2824. "#" + config.get("room_display_id")
  2825. ) # 要从中检索消息的频道,注意#必须携带在头部 The channel you want to retrieve messages from
  2826. token = config.get(
  2827. "twitch", "token"
  2828. ) # 访问 https://twitchapps.com/tmi/ 获取
  2829. user = config.get(
  2830. "twitch", "user"
  2831. ) # 你的Twitch用户名 Your Twitch username
  2832. # 代理服务器的地址和端口
  2833. proxy_server = config.get("twitch", "proxy_server")
  2834. proxy_port = int(config.get("twitch", "proxy_port"))
  2835. except Exception as e:
  2836. logger.error(traceback.format_exc())
  2837. logger.error("获取Twitch配置失败!\n{0}".format(e))
  2838. my_handle.abnormal_alarm_handle("platform")
  2839. # 配置代理服务器
  2840. socks.set_default_proxy(socks.HTTP, proxy_server, proxy_port)
  2841. # 创建socket对象
  2842. sock = socks.socksocket()
  2843. try:
  2844. sock.connect((server, port))
  2845. logger.info("成功连接 Twitch IRC server")
  2846. except Exception as e:
  2847. logger.error(traceback.format_exc())
  2848. logger.error(f"连接 Twitch IRC server 失败: {e}")
  2849. my_handle.abnormal_alarm_handle("platform")
  2850. sock.send(f"PASS {token}\n".encode("utf-8"))
  2851. sock.send(f"NICK {nickname}\n".encode("utf-8"))
  2852. sock.send(f"JOIN {channel}\n".encode("utf-8"))
  2853. regex = r":(\w+)!\w+@\w+\.tmi\.twitch\.tv PRIVMSG #\w+ :(.+)"
  2854. # 重连次数
  2855. retry_count = 0
  2856. while True:
  2857. try:
  2858. resp = sock.recv(2048).decode("utf-8")
  2859. # 输出所有接收到的内容,包括PING/PONG
  2860. # logger.info(resp)
  2861. if resp.startswith("PING"):
  2862. sock.send("PONG\n".encode("utf-8"))
  2863. elif not user in resp:
  2864. # 闲时计数清零
  2865. idle_time_auto_clear("comment")
  2866. resp = demojize(resp)
  2867. logger.debug(resp)
  2868. match = re.match(regex, resp)
  2869. username = match.group(1)
  2870. content = match.group(2)
  2871. content = content.rstrip()
  2872. logger.info(f"[{username}]: {content}")
  2873. data = {
  2874. "platform": platform,
  2875. "username": username,
  2876. "content": content,
  2877. }
  2878. my_handle.process_data(data, "comment")
  2879. except AttributeError as e:
  2880. logger.error(traceback.format_exc())
  2881. logger.error(f"捕获到异常: {e}")
  2882. logger.error("发生异常,重新连接socket")
  2883. my_handle.abnormal_alarm_handle("platform")
  2884. if retry_count >= 3:
  2885. logger.error(f"多次重连失败,程序结束!")
  2886. return
  2887. retry_count += 1
  2888. logger.error(f"重试次数: {retry_count}")
  2889. # 在这里添加重新连接socket的代码
  2890. # 例如,你可能想要关闭旧的socket连接,然后重新创建一个新的socket连接
  2891. sock.close()
  2892. # 创建socket对象
  2893. sock = socks.socksocket()
  2894. try:
  2895. sock.connect((server, port))
  2896. logger.info("成功连接 Twitch IRC server")
  2897. except Exception as e:
  2898. logger.error(f"连接 Twitch IRC server 失败: {e}")
  2899. sock.send(f"PASS {token}\n".encode("utf-8"))
  2900. sock.send(f"NICK {nickname}\n".encode("utf-8"))
  2901. sock.send(f"JOIN {channel}\n".encode("utf-8"))
  2902. except Exception as e:
  2903. logger.error(traceback.format_exc())
  2904. logger.error("Error receiving chat: {0}".format(e))
  2905. my_handle.abnormal_alarm_handle("platform")
  2906. except Exception as e:
  2907. logger.error(traceback.format_exc())
  2908. my_handle.abnormal_alarm_handle("platform")
  2909. elif platform == "wxlive":
  2910. import uvicorn
  2911. from fastapi import FastAPI, Request
  2912. from fastapi.middleware.cors import CORSMiddleware
  2913. from utils.models import SendMessage, LLMMessage, CallbackMessage, CommonResult
  2914. # 定义FastAPI应用
  2915. app = FastAPI()
  2916. seq_list = []
  2917. # 允许跨域
  2918. app.add_middleware(
  2919. CORSMiddleware,
  2920. allow_origins=["*"],
  2921. allow_credentials=True,
  2922. allow_methods=["*"],
  2923. allow_headers=["*"],
  2924. )
  2925. @app.post("/wxlive")
  2926. async def wxlive(request: Request):
  2927. global my_handle, config
  2928. try:
  2929. # 获取 POST 请求中的数据
  2930. data = await request.json()
  2931. # 这里可以添加代码处理接收到的数据
  2932. logger.debug(data)
  2933. if data["events"][0]["seq"] in seq_list:
  2934. return CommonResult(code=-1, message="重复数据过滤")
  2935. # 如果列表长度达到30,移除最旧的元素
  2936. if len(seq_list) >= 30:
  2937. seq_list.pop(0)
  2938. # 添加新元素
  2939. seq_list.append(data["events"][0]["seq"])
  2940. # 弹幕数据
  2941. if data["events"][0]["decoded_type"] == "comment":
  2942. # 闲时计数清零
  2943. idle_time_auto_clear("comment")
  2944. content = data["events"][0]["content"] # 获取弹幕内容
  2945. username = data["events"][0]["nickname"] # 获取发送弹幕的用户昵称
  2946. logger.info(f"[{username}]: {content}")
  2947. data = {
  2948. "platform": platform,
  2949. "username": username,
  2950. "content": content,
  2951. }
  2952. my_handle.process_data(data, "comment")
  2953. # 入场数据
  2954. elif data["events"][0]["decoded_type"] == "enter":
  2955. idle_time_auto_clear("entrance")
  2956. username = data["events"][0]["nickname"]
  2957. logger.info(f"用户:{username} 进入直播间")
  2958. # 添加用户名到最新的用户名列表
  2959. add_username_to_last_username_list(username)
  2960. data = {
  2961. "platform": platform,
  2962. "username": username,
  2963. "content": "进入直播间",
  2964. }
  2965. my_handle.process_data(data, "entrance")
  2966. pass
  2967. # 响应
  2968. return CommonResult(code=200, message="成功接收")
  2969. except Exception as e:
  2970. logger.error(traceback.format_exc())
  2971. my_handle.abnormal_alarm_handle("platform")
  2972. return CommonResult(code=-1, message=f"发送数据失败!{e}")
  2973. # 定义POST请求路径和处理函数
  2974. @app.post("/send")
  2975. async def send(msg: SendMessage):
  2976. global my_handle, config
  2977. try:
  2978. tmp_json = msg.dict()
  2979. logger.info(f"API收到数据:{tmp_json}")
  2980. data_json = tmp_json["data"]
  2981. if "type" not in data_json:
  2982. data_json["type"] = tmp_json["type"]
  2983. if data_json["type"] in ["reread", "reread_top_priority"]:
  2984. my_handle.reread_handle(data_json, type=data_json["type"])
  2985. elif data_json["type"] == "comment":
  2986. my_handle.process_data(data_json, "comment")
  2987. elif data_json["type"] == "tuning":
  2988. my_handle.tuning_handle(data_json)
  2989. elif data_json["type"] == "gift":
  2990. my_handle.gift_handle(data_json)
  2991. elif data_json["type"] == "entrance":
  2992. my_handle.entrance_handle(data_json)
  2993. return CommonResult(code=200, message="成功")
  2994. except Exception as e:
  2995. logger.error(f"发送数据失败!{e}")
  2996. return CommonResult(code=-1, message=f"发送数据失败!{e}")
  2997. @app.post("/llm")
  2998. async def llm(msg: LLMMessage):
  2999. global my_handle, config
  3000. try:
  3001. data_json = msg.dict()
  3002. logger.info(f"API收到数据:{data_json}")
  3003. resp_content = my_handle.llm_handle(
  3004. data_json["type"], data_json, webui_show=False
  3005. )
  3006. return CommonResult(
  3007. code=200, message="成功", data={"content": resp_content}
  3008. )
  3009. except Exception as e:
  3010. logger.error(f"调用LLM失败!{e}")
  3011. return CommonResult(code=-1, message=f"调用LLM失败!{e}")
  3012. @app.post("/callback")
  3013. async def callback(msg: CallbackMessage):
  3014. global my_handle, config, global_idle_time
  3015. try:
  3016. data_json = msg.dict()
  3017. logger.info(f"API收到数据:{data_json}")
  3018. # 音频播放完成
  3019. if data_json["type"] in ["audio_playback_completed"]:
  3020. # 如果等待播放的音频数量大于10
  3021. if data_json["data"]["wait_play_audio_num"] > int(
  3022. config.get("idle_time_task", "wait_play_audio_num_threshold")
  3023. ):
  3024. logger.info(
  3025. f'等待播放的音频数量大于限定值,闲时任务的闲时计时由 {global_idle_time} -> {int(config.get("idle_time_task", "idle_time_reduce_to"))}秒'
  3026. )
  3027. # 闲时任务的闲时计时 清零
  3028. global_idle_time = int(
  3029. config.get("idle_time_task", "idle_time_reduce_to")
  3030. )
  3031. return CommonResult(code=200, message="callback处理成功!")
  3032. except Exception as e:
  3033. logger.error(f"callback处理失败!{e}")
  3034. return CommonResult(code=-1, message=f"callback处理失败!{e}")
  3035. logger.info("HTTP API线程已启动!")
  3036. uvicorn.run(app, host="0.0.0.0", port=config.get("api_port"))
  3037. elif platform == "youtube":
  3038. import pytchat
  3039. def get_video_id():
  3040. try:
  3041. return config.get("room_display_id")
  3042. except Exception as e:
  3043. logger.error("获取直播间号失败!\n{0}".format(e))
  3044. return None
  3045. def process_chat(live):
  3046. while live.is_alive():
  3047. try:
  3048. for c in live.get().sync_items():
  3049. # 过滤表情包
  3050. chat_raw = re.sub(r":[^\s]+:", "", c.message)
  3051. chat_raw = chat_raw.replace("#", "")
  3052. if chat_raw != "":
  3053. # 闲时计数清零
  3054. idle_time_auto_clear("comment")
  3055. content = chat_raw # 获取弹幕内容
  3056. username = c.author.name # 获取发送弹幕的用户昵称
  3057. logger.info(f"[{username}]: {content}")
  3058. data = {
  3059. "platform": platform,
  3060. "username": username,
  3061. "content": content,
  3062. }
  3063. my_handle.process_data(data, "comment")
  3064. # time.sleep(1)
  3065. except Exception as e:
  3066. logger.error(traceback.format_exc())
  3067. logger.error("Error receiving chat: {0}".format(e))
  3068. my_handle.abnormal_alarm_handle("platform")
  3069. break # 退出内部while循环以触发重连机制
  3070. try:
  3071. reconnect_attempts = 0
  3072. last_reconnect_time = None
  3073. while True:
  3074. video_id = get_video_id()
  3075. if video_id is None:
  3076. break
  3077. live = pytchat.create(video_id=video_id)
  3078. process_chat(live)
  3079. current_time = time.time()
  3080. # 如果重连间隔只有30s内,那就只有3次,如果间隔大于30s,那就无限重连
  3081. if last_reconnect_time and (current_time - last_reconnect_time < 30):
  3082. reconnect_attempts += 1
  3083. if reconnect_attempts >= 3:
  3084. logger.error("重连失败次数已达上限,退出程序...")
  3085. break
  3086. logger.warning(
  3087. f"连接已关闭,间隔小于30秒,尝试重新连接 ({reconnect_attempts}/3)..."
  3088. )
  3089. else:
  3090. reconnect_attempts = 0 # 重置重连次数
  3091. logger.warning("连接已关闭,尝试重新连接...")
  3092. last_reconnect_time = current_time
  3093. except KeyboardInterrupt:
  3094. logger.warning("程序被强行退出")
  3095. finally:
  3096. logger.warning("关闭连接...")
  3097. os._exit(0)
  3098. elif platform == "hntv":
  3099. import requests
  3100. # 初始化已获取的commentId集合
  3101. comment_set = set()
  3102. def fetch_comments():
  3103. try:
  3104. url = f"https://pubmod.hntv.tv/dx-bridge/get-comment-with-article-super-v2?limit=40&typeId=1&appFusionId=1390195608019869697&page=1&objectId={my_handle.get_room_id()}"
  3105. response = requests.get(url)
  3106. if response.status_code == 200:
  3107. data = response.json()
  3108. items = data.get("result", {}).get("items", [])
  3109. for item in items:
  3110. comment_id = item.get("commentId")
  3111. if comment_id not in comment_set:
  3112. comment_set.add(comment_id)
  3113. username = item.get("commentUserNickname", "")
  3114. content = item.get("content", "")
  3115. logger.info(f"[{username}]: {content}")
  3116. data = {
  3117. "platform": platform,
  3118. "username": username,
  3119. "content": content,
  3120. }
  3121. my_handle.process_data(data, "comment")
  3122. else:
  3123. logger.error("获取弹幕数据失败。。。")
  3124. except Exception as e:
  3125. logger.error(traceback.format_exc())
  3126. my_handle.abnormal_alarm_handle("platform")
  3127. while True:
  3128. fetch_comments()
  3129. time.sleep(3) # 每隔3秒轮询一次
  3130. elif platform == "talk":
  3131. thread.join()
  3132. # 退出程序
  3133. def exit_handler(signum, frame):
  3134. logger.info("收到信号:", signum)
  3135. if __name__ == "__main__":
  3136. common = Common()
  3137. config = Config(config_path)
  3138. # 日志文件路径
  3139. log_path = "./log/log-" + common.get_bj_time(1) + ".txt"
  3140. # Configure_logger(log_path)
  3141. platform = config.get("platform")
  3142. if platform == "bilibili2":
  3143. from typing import Optional
  3144. # 这里填一个已登录账号的cookie。不填cookie也可以连接,但是收到弹幕的用户名会打码,UID会变成0
  3145. SESSDATA = ""
  3146. session: Optional[aiohttp.ClientSession] = None
  3147. elif platform == "dy2":
  3148. from protobuf.douyin import *
  3149. # 按键监听相关
  3150. do_listen_and_comment_thread = None
  3151. stop_do_listen_and_comment_thread_event = None
  3152. # 存储加载的模型对象
  3153. faster_whisper_model = None
  3154. sense_voice_model = None
  3155. # 正在录音中 标志位
  3156. is_recording = False
  3157. # 聊天是否唤醒
  3158. is_talk_awake = False
  3159. # 待播放音频数量(在使用 音频播放器 或者 metahuman-stream等不通过AI Vtuber播放音频的对接项目时,使用此变量记录是是否还有音频没有播放完)
  3160. wait_play_audio_num = 0
  3161. # 信号特殊处理
  3162. signal.signal(signal.SIGINT, exit_handler)
  3163. signal.signal(signal.SIGTERM, exit_handler)
  3164. start_server()