liqian 2 gadi atpakaļ
vecāks
revīzija
87731709b0
14 mainītis faili ar 1735 papildinājumiem un 0 dzēšanām
  1. 6 0
      .gitignore
  2. 126 0
      app.py
  3. 23 0
      audio_process.py
  4. 204 0
      config.py
  5. 344 0
      db_helper.py
  6. 94 0
      feishu.py
  7. 73 0
      gpt_process.py
  8. 280 0
      gpt_tag.py
  9. 40 0
      log.py
  10. 87 0
      log_conf.py
  11. 171 0
      main_process.py
  12. 80 0
      temporary_process.py
  13. 66 0
      utils.py
  14. 141 0
      xunfei_asr.py

+ 6 - 0
.gitignore

@@ -58,3 +58,9 @@ docs/_build/
 # PyBuilder
 target/
 
+.DS_Store
+.idea/
+videos/
+logs/
+
+

+ 126 - 0
app.py

@@ -0,0 +1,126 @@
+import random
+import os
+import logging
+import json
+import time
+import traceback
+import ast
+from gevent import monkey
+monkey.patch_all()
+
+from flask import Flask, request
+from mq_http_sdk.mq_exception import MQExceptionBase
+from mq_http_sdk.mq_producer import *
+from mq_http_sdk.mq_client import *
+from gpt_process import title_generate
+from log import Log
+from config import set_config
+from db_helper import RedisHelper
+from gevent.pywsgi import WSGIServer
+from multiprocessing import cpu_count, Process
+# from werkzeug.middleware.profiler import ProfilerMiddleware
+# from geventwebsocket.handler import WebSocketHandler
+
+app = Flask(__name__)
+log_ = Log()
+config_ = set_config()
+
+
+def title_generate_main():
+    # 初始化client
+    mq_client = MQClient(
+        # 设置HTTP协议客户端接入点
+        host=config_.MQ_CONFIG['ENDPOINT'],
+        # AccessKey ID,阿里云身份验证标识
+        access_id=config_.MQ_CONFIG['ACCESS_KEY'],
+        # AccessKey Secret,阿里云身份验证密钥
+        access_key=config_.MQ_CONFIG['SECRET_KEY']
+    )
+    # Topic所属的实例ID,在消息队列RocketMQ版控制台创建。
+    # 若实例有命名空间,则实例ID必须传入;若实例无命名空间,则实例ID传入空字符串。实例的命名空间可以在消息队列RocketMQ版控制台的实例详情页面查看。
+    instance_id = config_.MQ_CONFIG['INSTANCE_ID']
+    # 监听消息所属的Topic
+    todo_topic_name = config_.MQ_TOPIC_CONFIG['asr_title_todo']['topic_name']
+    # 您在消息队列RocketMQ版控制台创建的Group ID。
+    group_id = config_.MQ_TOPIC_CONFIG['asr_title_todo']['group_id']
+    consumer = mq_client.get_consumer(instance_id, todo_topic_name, group_id)
+    # 长轮询表示如果Topic没有消息,则客户端请求会在服务端挂起3秒,3秒内如果有消息可以消费则立即返回响应。
+    # 长轮询时间3秒(最多可设置为30秒)。
+    wait_seconds = 3
+    # 一次最多消费1条(最多可设置为16条)。
+    batch = 1
+    print(("%sConsume And Ak Message From Topic%s\nTopicName:%s\nMQConsumer:%s\nWaitSeconds:%s\n" \
+           % (10 * "=", 10 * "=", todo_topic_name, group_id, wait_seconds)))
+    while True:
+        receipt_handle_list = []
+        try:
+            # 长轮询消费消息。
+            recv_msgs = consumer.consume_message(batch, wait_seconds)
+            for msg in recv_msgs:
+                print(("Receive, MessageId: %s\nMessageBodyMD5: %s \
+                                  \nMessageTag: %s\nConsumedTimes: %s \
+                                  \nPublishTime: %s\nBody: %s \
+                                  \nNextConsumeTime: %s \
+                                  \nReceiptHandle: %s \
+                                  \nProperties: %s\n" % \
+                       (msg.message_id, msg.message_body_md5,
+                        msg.message_tag, msg.consumed_times,
+                        msg.publish_time, msg.message_body,
+                        msg.next_consume_time, msg.receipt_handle, msg.properties)))
+                video_id = msg.message_body['videoId']
+                video_path = msg.message_body['videoPath']
+                try:
+                    title = title_generate(video_id=video_id, video_path=video_path)
+                except ConnectionResetError:
+                    # API限流
+                    log_.info(video_id)
+                    # 记录重试次数
+                    key_name = f"{config_.TITLE_GENERATE_RETRY_KEY_NAME_PREFIX}{video_id}"
+                    redis_helper = RedisHelper()
+                    redis_helper.setnx_key(key_name=key_name, value=0, expire_time=2*3600)
+                    redis_helper.incr_key(key_name=key_name, amount=1, expire_time=2*3600)
+                    # 判断已重试次数
+                    retry_count = redis_helper.get_data_from_redis(key_name=key_name)
+                    if retry_count is not None and retry_count == config_.RETRY_MAX_COUNT:
+                        # 确认消息消费成功
+                        receipt_handle_list.append(msg.receipt_handle)
+                        pass
+                    else:
+                        pass
+
+                except Exception:
+                    # 确认消息消费成功
+                    receipt_handle_list.append(msg.receipt_handle)
+                    log_.info(traceback.format_exc())
+                else:
+                    # 1. 发送结果至done消息队列
+                    print(title)
+                    # 2. 确认消息消费成功
+                    receipt_handle_list.append(msg.receipt_handle)
+
+        except MQExceptionBase as e:
+            # Topic中没有消息可消费。
+            if e.type == "MessageNotExist":
+                print(("No new message! RequestId: %s" % e.req_id))
+                continue
+
+            print(("Consume Message Fail! Exception:%s\n" % e))
+            time.sleep(2)
+            continue
+
+        # msg.next_consume_time前若不确认消息消费成功,则消息会被重复消费。
+        # 消息句柄有时间戳,同一条消息每次消费拿到的都不一样。
+        try:
+            consumer.ack_message(receipt_handle_list)
+            print(("Ak %s Message Succeed.\n\n" % len(receipt_handle_list)))
+        except MQExceptionBase as e:
+            print(("\nAk Message Fail! Exception:%s" % e))
+            # 某些消息的句柄可能超时,会导致消息消费状态确认不成功。
+            if e.sub_errors:
+                for sub_error in e.sub_errors:
+                    print(("\tErrorHandle:%s,ErrorCode:%s,ErrorMsg:%s" % \
+                           (sub_error["ReceiptHandle"], sub_error["ErrorCode"], sub_error["ErrorMessage"])))
+
+
+if __name__ == '__main__':
+    title_generate_main()

+ 23 - 0
audio_process.py

@@ -0,0 +1,23 @@
+from moviepy.editor import AudioFileClip, VideoFileClip
+from config import set_config
+
+config_ = set_config()
+
+
+def get_wav(video_path):
+    """提取音频"""
+    video = VideoFileClip(video_path)
+    # Extract the audio from the video
+    audio = video.audio
+    # Save the extracted audio to a file
+    audio_path = video_path.replace('.mp4', '.wav')
+    audio.write_audiofile(audio_path)
+    return audio_path
+
+
+def get_audio_duration(audio_file_path):
+    """获取音频时长,单位:ms"""
+    audio_clip = AudioFileClip(audio_file_path)
+    audio_length = audio_clip.duration
+    return int(audio_length * 1000)
+

+ 204 - 0
config.py

@@ -0,0 +1,204 @@
+import os
+
+
+class BaseConfig(object):
+    # 讯飞asr配置
+    XFASR_HOST = 'https://raasr.xfyun.cn/v2/api'
+    XF_API = {
+        'upload': '/upload',
+        'get_result': '/getResult'
+    }
+    XFASR_CONFIG = {
+        'appid': 'ac4ec700',
+        'secret_key': 'f822c63011275bcd26fa286fbb01768a'
+    }
+
+    # gpt配置
+    GPT_HOST = 'https://api.openai.com/v1/chat/completions'
+    GPT_OPENAI_API_KEY = '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'
+    # 代理地址
+    PROXIES = {
+        'http': 'http://127.0.0.1:4780',
+        'https': 'http://127.0.0.1:4780'
+    }
+
+    # MQ配置
+    MQ_CONFIG = {
+        'ENDPOINT': 'http://1894469520484605.mqrest.cn-qingdao-public.aliyuncs.com',
+        'ACCESS_KEY': 'LTAI4G7puhXtLyHzHQpD6H7A',
+        'SECRET_KEY': 'nEbq3xWNQd1qLpdy2u71qFweHkZjSG',
+        'INSTANCE_ID': 'MQ_INST_1894469520484605_BXhXuzkZ'
+    }
+
+    # 飞书应用凭证
+    FEISHU_TOKEN = {
+        'app_id': 'cli_a3667697a57b500e',
+        'app_secret': '5eMszgeNt21U56XnPjCykgmTfZUEEMnp'
+    }
+
+    # 记录生成标题重试次数
+    TITLE_GENERATE_RETRY_KEY_NAME_PREFIX = 'title:generate:retry:count:'
+
+    # video tags
+    TAGS = ['舞蹈', '美食', '时尚', '旅行', '音乐', '运动', '影视', '搞笑', '科技', '综艺',
+            '游戏', '情感', '健康', '人文', '社会', '热点', '财富', '生活']
+    # GPT prompt
+    GPT_PROMPT = {
+        'tags': {
+            'prompt1': f"""
+请对如下文本进行分类。类别为其中的一个:【{' '.join(TAGS)}】。
+以json格式返回,key为category与confidence,分别代表类别与分类置信度。给出top 3的分类结果。
+-----------------------------
+""",
+            'prompt2': f"""
+请对如下文本进行:
+1. 分类,类别为其中的一个:【{' '.join(TAGS)}】。如果无法有效分类,请返回“其他”。
+2. 用20个字以内对文本内容进行概况。
+3. 为文本取一个易于分享,吸引人要求的标题。
+4. 列举三个关键词。
+以json格式返回,key为category, confidence, summery, title, keywords。分别代表类别,分类置信度,概要,标题,关键词。
+-----------------------------
+"""
+        },
+        'title': {
+            'prompt1': f"""
+请对如下文本进行: 为文本取一个易于分享,吸引人要求的标题。要求在30个字以内。
+-----------------------------
+""",
+            'prompt2': f"""
+我想让你充当爆款标题生成器,我会给你提供一段视频的讲解文本,你生成一个更吸引眼球的标题。
+标题不要超过35个字。
+标题要突出惊奇感,让老年人看到就想转发,语义要保持中立,不要向负面倾斜,不要涉及政治敏感话题。
+如果原讲解文本中有地名,不要改变讲解文本中的地名。
+如果原讲解文本是关于唱歌但是没有说明是哪首歌,你起的标题也不要说歌名。
+如果原讲解文本中没有明确指出男女性别,你起的标题也不要说具体的性别。
+如果原讲解文本中没有明确指出是谁干了这件事,你起的标题也不要说是谁干的。
+如果原讲解文本中没有提及老年人,你起的标题也不要说老年人。
+我的原讲解文本是:
+"""
+        }
+    }
+
+
+class DevelopmentConfig(BaseConfig):
+    """开发环境配置"""
+    # 报警内容 环境区分
+    ENV_TEXT = "开发环境"
+
+    # redis地址
+    REDIS_INFO = {
+        'host': 'r-bp1ps6my7lzg8rdhwx682.redis.rds.aliyuncs.com',
+        'port': 6379,
+        'password': 'Wqsd@2019',
+    }
+
+    # MQ TOPIC配置
+    MQ_TOPIC_CONFIG = {
+        'asr_title_todo': {
+            'topic_name': 'topic_asr_title_todo_test',
+            'group_id': 'GID_ASR_TITLE_TODO_TEST'
+        },
+        'asr_title_done': {
+            'topic_name': 'topic_asr_title_done_test',
+            'group_id': 'GID_ASR_TITLE_DONE_TEST'
+        },
+
+    }
+
+
+class TestConfig(BaseConfig):
+    """测试环境配置"""
+    # 报警内容 环境区分
+    ENV_TEXT = "测试环境"
+
+    # redis地址
+    REDIS_INFO = {
+        'host': 'r-bp1ps6my7lzg8rdhwx682.redis.rds.aliyuncs.com',
+        'port': 6379,
+        'password': 'Wqsd@2019',
+    }
+
+    # MQ TOPIC配置
+    MQ_TOPIC_CONFIG = {
+        'asr_title_todo': {
+            'topic_name': 'topic_asr_title_todo_test',
+            'group_id': 'GID_ASR_TITLE_TODO_TEST'
+        },
+        'asr_title_done': {
+            'topic_name': 'topic_asr_title_done_test',
+            'group_id': 'GID_ASR_TITLE_DONE_TEST'
+        },
+
+    }
+
+
+class PreProductionConfig(BaseConfig):
+    """预发布环境配置"""
+    # 报警内容 环境区分
+    ENV_TEXT = "预发布环境"
+
+    # redis地址
+    REDIS_INFO = {
+        'host': 'r-bp1fogs2mflr1ybfot.redis.rds.aliyuncs.com',
+        'port': 6379,
+        'password': 'Wqsd@2019',
+    }
+
+    # MQ TOPIC配置
+    MQ_TOPIC_CONFIG = {
+        'asr_title_todo': {
+            'topic_name': 'topic_asr_title_todo',
+            'group_id': 'GID_ASR_TITLE_TODO'
+        },
+        'asr_title_done': {
+            'topic_name': 'topic_asr_title_done',
+            'group_id': 'GID_ASR_TITLE_DONE'
+        },
+
+    }
+
+
+class ProductionConfig(BaseConfig):
+    """生产环境配置"""
+    # 报警内容 环境区分
+    ENV_TEXT = "生产环境"
+
+    # redis地址
+    REDIS_INFO = {
+        'host': 'r-bp1fogs2mflr1ybfot.redis.rds.aliyuncs.com',
+        'port': 6379,
+        'password': 'Wqsd@2019',
+    }
+
+    # MQ TOPIC配置
+    MQ_TOPIC_CONFIG = {
+        'asr_title_todo': {
+            'topic_name': 'topic_asr_title_todo',
+            'group_id': 'GID_ASR_TITLE_TODO'
+        },
+        'asr_title_done': {
+            'topic_name': 'topic_asr_title_done',
+            'group_id': 'GID_ASR_TITLE_DONE'
+        },
+
+    }
+
+
+def set_config():
+    # 获取环境变量 ROV_OFFLINE_ENV
+    # env = os.environ.get('ROV_OFFLINE_ENV')
+    env = 'dev'
+    if env is None:
+        # log_.error('ENV ERROR: is None!')
+        return
+    if env == 'dev':
+        return DevelopmentConfig()
+    elif env == 'test':
+        return TestConfig()
+    elif env == 'pre':
+        return PreProductionConfig()
+    elif env == 'pro':
+        return ProductionConfig()
+    else:
+        # log_.error('ENV ERROR: is {}'.format(env))
+        return

+ 344 - 0
db_helper.py

@@ -0,0 +1,344 @@
+import traceback
+import time
+import redis
+import pymysql
+from config import set_config
+from log import Log
+
+config_ = set_config()
+log_ = Log()
+
+conn_redis = None
+
+
+class RedisHelper(object):
+    def __init__(self, params=None):
+        """
+        初始化redis连接信息
+        redis_info: redis连接信息, 格式:dict, {'host': '', 'port': '', 'password': ''}
+        """
+        redis_info = config_.REDIS_INFO
+        self.host = redis_info['host']
+        self.port = redis_info['port']
+        self.password = redis_info['password']
+        self.params = params
+
+    def connect(self):
+        """
+        连接redis
+        :return: conn
+        """
+        global conn_redis
+        if conn_redis is None:
+            pool = redis.ConnectionPool(host=self.host,
+                                        port=self.port,
+                                        password=self.password,
+                                        decode_responses=True)
+            conn = redis.Redis(connection_pool=pool)
+            conn_redis = conn
+        return conn_redis
+
+    def key_exists(self, key_name):
+        """
+        判断key是否存在
+        :param key_name: key
+        :return: 存在-True, 不存在-False
+        """
+        conn = self.connect()
+        res = conn.exists(key_name)
+        return res
+
+    def del_keys(self, key_name):
+        """
+        删除key
+        :param key_name: key
+        :return: None
+        """
+        conn = self.connect()
+        conn.delete(key_name)
+
+    def get_data_from_redis(self, key_name):
+        """
+        读取redis中的数据
+        :param key_name: key
+        :return: data
+        """
+        conn = self.connect()
+        if not conn.exists(key_name):
+            # key不存在
+            return None
+        data = conn.get(key_name)
+        return data
+
+    def set_data_to_redis(self, key_name, value, expire_time=24*3600):
+        """
+        新增数据
+        :param key_name: key
+        :param value: 元素的值 videoId
+        :param expire_time: 过期时间,单位:s,默认1天
+        :return: None
+        """
+        conn = self.connect()
+        conn.set(key_name, value, ex=int(expire_time))
+
+    def add_data_with_zset(self, key_name, data, expire_time=7*24*3600):
+        """
+        新增数据,有序set
+        :param key_name: key
+        :param data: 元素的值及对应分数 type-dict  {value: score}
+        :param expire_time: 过期时间,单位:s,默认7天,type-int
+        :return: None
+        """
+        conn = self.connect()
+        conn.zadd(key_name, data)
+        # 设置过期时间
+        conn.expire(key_name, int(expire_time))
+
+    def get_data_zset_with_index(self, key_name, start, end, desc=True, with_scores=False):
+        """
+        根据索引位置获取元素的值
+        :param key_name: key
+        :param start: 索引起始点 闭区间,包含start
+        :param end: 索引结束点 闭区间,包含end
+        :param desc: 分数排序方式,默认从大到小
+        :param with_scores: 是否获取元素的分数,默认 False,只获取元素的值
+        :return: data 元素值列表(不包含分数),value(videoId)类型转换为int, 包含分数时不进行类型转换
+        """
+        conn = self.connect()
+        if not conn.exists(key_name):
+            return None
+        data = conn.zrange(key_name, start, end, desc, with_scores)
+        if with_scores:
+            data = data
+        else:
+            data = [eval(value) for value in data]
+
+        return data
+
+    def get_all_data_from_zset(self, key_name, desc=True, with_scores=False):
+        """
+        获取zset中所有元素的值
+        :param key_name: key
+        :param desc: 分数排序方式,默认从大到小
+        :param with_scores: 是否获取元素的分数,默认 False,只获取元素的值
+        :return: data 元素值列表(不包含分数),value(videoId)类型转换为int, 包含分数时不进行类型转换
+        """
+        conn = self.connect()
+        if not conn.exists(key_name):
+            return None
+        data = []
+        start = 0
+        step = 100
+        while True:
+            end = start + step - 1
+            temp = conn.zrange(key_name, start, end, desc, with_scores)
+            if not temp:
+                break
+            data.extend(temp)
+            start += step
+        return data
+
+    def get_score_with_value(self, key_name, value):
+        """
+        在zset中,根据元素的value获取对应的score
+        :param key_name: key
+        :param value: 元素的值
+        :return: score value对应的score
+        """
+        conn = self.connect()
+        if not conn.exists(key_name):
+            return None
+        return conn.zscore(key_name, value)
+
+    def get_rank_with_value(self, key_name, value, desc=False):
+        """
+        在zset中,根据元素的value获取对应排名
+        :param key_name: key
+        :param value: 元素的值
+        :param desc: 是否倒序 type-bool 默认:False-按照score从小到大
+        :return: rank value对应的rank,从0开始,不存在返回None
+        """
+        conn = self.connect()
+        if not conn.exists(key_name):
+            return None
+        if desc is True:
+            return conn.zrevrank(key_name, value)
+        else:
+            return conn.zrank(key_name, value)
+
+    def update_score_with_value(self, key_name, value, score, expire_time=24*3600):
+        """
+        在zset中,修改元素value对应的score
+        :param key_name: key
+        :param value: 元素的值
+        :param score: value对应的score更新值
+        :param expire_time: 过期时间,单位:s,默认1天,type-int
+        """
+        conn = self.connect()
+        if conn.exists(key_name):
+            conn.zadd(key_name, {value: score})
+        else:
+            # key不存在时,需设置过期时间
+            conn.zadd(key_name, {value: score})
+            conn.expire(key_name, int(expire_time))
+
+    def remove_value_from_zset(self, key_name, value):
+        """
+        删除zset中的指定元素
+        :param key_name: key
+        :param value: 元素的值
+        :return: None
+        """
+        conn = self.connect()
+        res = conn.zrem(key_name, value)
+        return res
+
+    def get_index_with_data(self, key_name, value):
+        """
+        根据元素的值获取在有序set中的位置,按照分数倒序(从大到小)
+        :param key_name: key
+        :param value: 元素的值
+        :return: idx 位置索引
+        """
+        conn = self.connect()
+        res = conn.zrevrank(key_name, value)
+        return res
+
+    def get_data_from_set(self, key_name):
+        """
+        获取set中的所有数据
+        :param key_name: key
+        :return: data
+        """
+        conn = self.connect()
+        if not conn.exists(key_name):
+            # key不存在
+            return None
+        data = []
+        cursor = 0
+        while True:
+            cur, temp = conn.sscan(key_name, cursor=cursor, count=2000)
+            data.extend(temp)
+            if cur == 0:
+                break
+            cursor = cur
+        return list(set(data))
+
+    def add_data_with_set(self, key_name, values, expire_time=30*60):
+        """
+        新增数据,set
+        :param key_name: key
+        :param values: 要添加的元素  类型-tuple
+        :param expire_time: 过期时间,单位:s,默认0.5小时 type-int
+        :return: None
+        """
+        conn = self.connect()
+        conn.sadd(key_name, *values)
+        # 设置过期时间
+        conn.expire(key_name, int(expire_time))
+
+    def data_exists_with_set(self, key_name, value):
+        """
+        判断元素value是否在集合key_name中
+        :param key_name: key
+        :param value: 需判断的元素
+        :return: 存在-True, 不存在-False
+        """
+        conn = self.connect()
+        res = conn.sismember(key_name, value)
+        return res
+
+    def get_data_with_count_from_set(self, key_name, count=1):
+        """
+        从set中随机获取元素,并放回
+        :param key_name: key
+        :param count: 获取个数, 默认为1
+        :return:
+        """
+        conn = self.connect()
+        data = conn.srandmember(name=key_name, number=count)
+        return data
+
+    def remove_value_from_set(self, key_name, values):
+        """
+        删除set中的指定元素
+        :param key_name: key
+        :param values: 元素的值, 类型-tuple
+        :return: None
+        """
+        conn = self.connect()
+        conn.srem(key_name, *values)
+
+    def decr_key(self, key_name, amount=1, expire_time=30*60):
+        """
+        redis自减
+        :param key_name: key
+        :param amount: 自减数,默认为1,type-int
+        :param expire_time: 过期时间,单位:s,默认0.5小时 type-int
+        :return: None
+        """
+        conn = self.connect()
+        conn.decr(name=key_name, amount=amount)
+        conn.expire(key_name, int(expire_time))
+
+    def incr_key(self, key_name, amount=1, expire_time=30*60):
+        """
+        redis自增
+        :param key_name: key
+        :param amount: 自增数,默认为1,type-int
+        :param expire_time: 过期时间,单位:s,默认0.5小时 type-int
+        :return: None
+        """
+        conn = self.connect()
+        conn.incr(name=key_name, amount=amount)
+        conn.expire(key_name, int(expire_time))
+
+    def setnx_key(self, key_name, value, expire_time=5*60):
+        """
+        当key不存在时,将value塞入key中,key存在时不做操作
+        :param key_name: key
+        :param value: value
+        :return: 过期时间,单位:s,默认5分钟 type-int
+        """
+        conn = self.connect()
+        conn.setnx(name=key_name, value=value)
+        conn.expire(name=key_name, time=int(expire_time))
+
+
+class MysqlHelper(object):
+    def __init__(self):
+        """
+        初始化mysql连接信息
+        """
+        self.mysql_info = config_.MYSQL_INFO
+
+    def get_data(self, sql):
+        """
+        查询数据
+        :param sql: sql语句
+        :return: data
+        """
+        # 连接数据库
+        conn = pymysql.connect(**self.mysql_info)
+        # 创建游标
+        cursor = conn.cursor()
+        try:
+            # 执行SQL语句
+            cursor.execute(sql)
+            # 获取查询的所有记录
+            data = cursor.fetchall()
+        except Exception as e:
+            return None
+        # 关闭游标对象
+        cursor.close()
+        # 关闭数据库连接
+        conn.close()
+        return data
+
+
+if __name__ == '__main__':
+    redis_helper = RedisHelper()
+    res = redis_helper.remove_value_from_zset(
+        key_name="recall:item:score:region:dup3:24h:110000:data1:rule4:20230315:14",
+        value=111111)
+    print(res)

+ 94 - 0
feishu.py

@@ -0,0 +1,94 @@
+import json
+from utils import request_post, request_get
+from config import set_config
+
+config_ = set_config()
+
+
+class FeiShuHelper(object):
+    @staticmethod
+    def get_tenant_access_token():
+        """获取自建应用的tenant_access_token"""
+        url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
+        headers = {"Content-Type": "application/json; charset=utf-8"}
+        request_data = config_.FEISHU_TOKEN
+        data = request_post(request_url=url, headers=headers, request_data=request_data)
+        if data is not None:
+            tenant_access_token = data.get('tenant_access_token')
+            return tenant_access_token
+
+    def get_data(self, spreadsheet_token, sheet_id):
+        """读取电子表格数据"""
+        tenant_access_token = self.get_tenant_access_token()
+        url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_get"
+        headers = {
+            "Content-Type": "application/json; charset=utf-8",
+            "Authorization": f"Bearer {tenant_access_token}"
+        }
+        params = {
+            'ranges': sheet_id,
+        }
+        data = request_get(request_url=url, headers=headers, params=params)
+        values = []
+        if data is not None:
+            try:
+                values = data['data']['valueRanges'][0].get('values')
+            except:
+                values = []
+        return values
+
+    def data_to_feishu_sheet(self, sheet_token, sheet_id, data, start_row, start_column, end_column):
+        tenant_access_token = self.get_tenant_access_token()
+        url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{sheet_token}/values_prepend"
+        headers = {
+            "Content-Type": "application/json",
+            "Authorization": f"Bearer {tenant_access_token}"
+        }
+        for i in range(len(data) // 10 + 1):
+            values = data[i * 10:(i + 1) * 10]
+            start_index = start_row + i * 10
+            end_index = start_index + len(values)-1
+            print(len(values), start_index, end_index)
+            post_data = {
+                "valueRange": {
+                    "range": f"{sheet_id}!{start_column}{start_index}:{end_column}{end_index}",
+                    "values": values
+                }
+            }
+            r2 = request_post(request_url=url, headers=headers, request_data=post_data)
+            # print(r2["msg"])
+            print(r2)
+
+    def update_values(self, sheet_token, sheet_id, data, start_row, start_column, end_column):
+        """写入数据"""
+        tenant_access_token = self.get_tenant_access_token()
+        url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{sheet_token}/values_append"
+        headers = {
+            "Content-Type": "application/json",
+            "Authorization": f"Bearer {tenant_access_token}"
+        }
+        for i in range(len(data) // 10 + 1):
+            values = data[i * 10:(i + 1) * 10]
+            start_index = start_row + i * 10
+            end_index = start_index + len(values) - 1
+            print(len(values), start_index, end_index)
+            post_data = {
+                "valueRange": {
+                    "range": f"{sheet_id}!{start_column}{start_index}:{end_column}{end_index}",
+                    "values": values
+                }
+            }
+            r2 = request_post(request_url=url, headers=headers, request_data=post_data)
+            # print(r2["msg"])
+            print(r2)
+
+
+if __name__ == '__main__':
+    # sheet_info = config_.SHEET_INFO['汉语常用词汇表']
+    # FeiShuHelper().get_data(spreadsheet_token=sheet_info.get('spreadsheet_token'), sheet_id=sheet_info.get('sheet_id'))
+    FeiShuHelper().data_to_feishu_sheet(sheet_token='DkiUsqwJ6hmBxstBYyEcNE4ante',
+                                        sheet_id='08d4cc',
+                                        data=[['1', 2, 3, 4]],
+                                        start_row=1,
+                                        start_column='A',
+                                        end_column='D')

+ 73 - 0
gpt_process.py

@@ -0,0 +1,73 @@
+import time
+import requests
+import traceback
+from utils import download_video
+from audio_process import get_wav
+from xunfei_asr import call_asr
+from config import set_config
+from log import Log
+
+config_ = set_config()
+log_ = Log()
+
+
+def request_gpt(prompt):
+    headers = {
+        'Content-Type': 'application/json',
+        'Authorization': f'Bearer {config_.GPT_OPENAI_API_KEY}',
+    }
+    proxies = config_.PROXIES
+
+    json_data = {
+        'model': 'gpt-3.5-turbo',
+        'messages': [
+            {
+                'role': 'user',
+                'content': f'{prompt}',
+            },
+        ],
+    }
+    raise ConnectionResetError
+    response = requests.post(url=config_.GPT_HOST, headers=headers, json=json_data, proxies=proxies)
+    # print(response.json())
+    # print(response.json()['choices'][0]['message']['content'])
+    # print('\n')
+    result_content = response.json()['choices'][0]['message']['content']
+    return result_content
+
+
+def title_generate(video_id, video_path):
+    """
+    视频生成标题
+    :param video_id: videoId
+    :param video_path: videoPath
+    :return:
+    """
+    # 1. 下载视频
+    video_file_path = download_video(video_path=video_path, video_id=video_id, download_folder='videos')
+    log_.info(f"video_file_path = {video_file_path}")
+    # 2. 获取视频中的音频
+    audio_path = get_wav(video_path=video_file_path)
+    log_.info(f"audio_path = {audio_path}")
+    # 3. asr
+    dialogue_path, asr_res = call_asr(audio_path=audio_path)
+    log_.info(f"dialogue_path = {dialogue_path}, asr_res = {asr_res}")
+    # 4. gpt产出结果
+    prompt = f"{config_.GPT_PROMPT['title']['prompt2']}{asr_res.strip()}"
+    gpt_res = request_gpt(prompt=prompt)
+    return gpt_res
+    # except ConnectionResetError:
+    #     log_.info(video_id)
+    # except Exception as e:
+    #     log_.info(traceback.format_exc())
+    # else:
+    #     print(gpt_res)
+
+
+    # print(gpt_res)
+    # log_.info(f"gpt_res = {gpt_res}")
+
+
+
+if __name__ == '__main__':
+    title_generate(video_id='001', video_path='')

+ 280 - 0
gpt_tag.py

@@ -0,0 +1,280 @@
+import time
+import requests
+import traceback
+from config import set_config
+from log import Log
+
+config_ = set_config()
+log_ = Log()
+
+
+def get_tag(text):
+    retry = 1
+    while retry < 3:
+        try:
+            log_.info(f"retry = {retry}")
+            # tags = ['舞蹈', '美食', '时尚', '旅行', '音乐', '运动', '影视', '搞笑', '科技', '综艺', '游戏', '情感', '健康', '人文', '社会', '热点', '财富', '生活']
+            # # tags = ["惊奇","有趣","有用","同感"]
+            # prompt = f"""请对如下文本进行分类。类别为其中的一个:【{' '.join(tags)}】。以json格式返回,key为category与confidence,分别代表类别与分类置信度。给出top 3的分类结果。
+            # -----------------------------
+            # {text}"""
+
+            # prompt = f"""请对如下文本进行:1. 分类,类别为其中的一个:【{' '.join(tags)}】。如果无法有效分类,请返回“其他”。2. 用20个字以内对文本内容进行概况。3. 为文本取一个易于分享,吸引人要求的标题。4. 列举三个关键词。
+            #     以json格式返回,key为category, confidence, summery, title, keywords。分别代表类别,分类置信度,概要,标题,关键词。
+            #     -----------------------------
+            #     {text}"""
+            # prompt = f"""
+            # 请对如下文本进行以下4个操作:
+            # 1. 分类,请给出top 3的分类结果与分类置信度,如果无法有效分类,请返回“其他”。以json array格式返回,key为category与confidence,分别代表类别与分类置信度。
+            # 2. 用20个字以内对文本内容进行概况。
+            # 3. 为文本取一个易于分享,吸引人要求的标题。
+            # 4. 列举三个关键词。
+            # 将结果以json格式返回,key为category_res, summery, title, keywords,分别代表分类,概要,标题,关键词。
+            # -----------------------------
+            # {text}
+            # """
+            # prompt = f"""
+            #     请对如下文本进行: 为文本取一个易于分享,吸引人要求的标题。要求在30个字以内。
+            #     -----------------------------
+            #     {text}
+            #     """
+
+            # prompt = f"""
+            # 我想让你充当爆款标题生成器,我会给你提供一段视频的讲解文本,你生成一个更吸引眼球的标题。
+            # 标题要突出惊奇感,让老年人看到就想转发,语义要保持中立,不要向负面倾斜,不要涉及政治敏感话题。
+            # 如果原讲解文本中有地名,不要改变讲解文本中的地名。
+            # 如果原讲解文本是关于唱歌但是没有说明是哪首歌,你起的标题也不要说歌名。
+            # 如果原讲解文本中没有明确指出男女性别,你起的标题也不要说具体的性别。
+            # 如果原讲解文本中没有明确指出是谁干了这件事,你起的标题也不要说是谁干的。
+            # 如果原讲解文本中没有提及老年人,你起的标题也不要说老年人。
+            # 我的原讲解文本是:
+            # {text}
+            # """
+            # prompt = f"""
+            # 我想让你充当爆款标题生成器,我会给你提供一段视频的讲解文本,你生成一个更吸引眼球,并且字数在35个字以内的标题。
+            # 标题要突出惊奇感,让老年人看到就想转发,语义要保持中立,不要向负面倾斜,不要涉及政治敏感话题。
+            # 如果原讲解文本中有地名,不要改变讲解文本中的地名。
+            # 如果原讲解文本是关于唱歌但是没有说明是哪首歌,你起的标题也不要说歌名。
+            # 如果原讲解文本中没有明确指出男女性别,你起的标题也不要说具体的性别。
+            # 如果原讲解文本中没有明确指出是谁干了这件事,你起的标题也不要说是谁干的。
+            # 如果原讲解文本中没有提及老年人,你起的标题也不要说老年人。
+            # 我的原讲解文本是:
+            # {text}
+            # """
+            prompt = f"""
+            我想让你充当爆款标题生成器,我会给你提供一段视频的讲解文本,你生成一个更吸引眼球的标题。
+            标题不要超过35个字。
+            标题要突出惊奇感,让老年人看到就想转发,语义要保持中立,不要向负面倾斜,不要涉及政治敏感话题。
+            如果原讲解文本中有地名,不要改变讲解文本中的地名。
+            如果原讲解文本是关于唱歌但是没有说明是哪首歌,你起的标题也不要说歌名。
+            如果原讲解文本中没有明确指出男女性别,你起的标题也不要说具体的性别。
+            如果原讲解文本中没有明确指出是谁干了这件事,你起的标题也不要说是谁干的。
+            如果原讲解文本中没有提及老年人,你起的标题也不要说老年人。
+            我的原讲解文本是:
+            {text}
+            """
+
+            headers = {
+                'Content-Type': 'application/json',
+                # 'Authorization': f'Bearer {os.environ["OPENAI_API_KEY"]}',
+                'Authorization': f'Bearer {config_.GPT_OPENAI_API_KEY}',
+            }
+            proxies = config_.PROXIES
+
+            json_data = {
+                'model': 'gpt-3.5-turbo',
+                'messages': [
+                    {
+                        'role': 'user',
+                        'content': f'{prompt}',
+                    },
+                ],
+            }
+            response = requests.post(url=config_.GPT_HOST, headers=headers, json=json_data, proxies=proxies)
+            print(response.json())
+            print(response.json()['choices'][0]['message']['content'])
+            print('\n')
+            result_content = response.json()['choices'][0]['message']['content']
+            return result_content
+        except Exception as e:
+            print(e)
+            log_.error(traceback.format_exc())
+            retry += 1
+            time.sleep(60)
+            continue
+
+
+if __name__ == '__main__':
+    text = """请你用15分钟一口气说完中国史,
+计时开始。
+中国历史上第一个朝代是夏朝夏王,大禹联合了各部落,建立了国家的雏形,大禹的儿子启是夏朝的第一任国君,
+也就是说雨是筹备者,岂是开国者,下棋死后儿子斯泰康世袭皇位,
+泰康爱好打猎,经常几个月不回家,就导致了泰康失国打猎回来的路上被后羿给赶走了。
+后裔篡下朝八年,后裔手下有个狠人叫做韩卓,
+在后裔60岁时又干掉了后裔,
+在位60年后被大禹的后代少康率军杀回来干掉了,
+并开启了中国历史上第一个中兴时代,绍康中兴夏朝平稳发展,直到夏朝最后一个君主夏杰,
+这是个暴君引起了民愤,被商部落的首领商汤所灭。
+商汤建立商朝,商汤有个著名的军师叫伊尹,也就是道德经中说
+治大国若烹小鲜的那位神人。
+500多年后,商朝最后一任君主就是历史上有名的暴君纣王了。
+纣王被周部落的一个叫姬发的人,率军在牧野之战中给灭了,纣王自焚于鹿台,也就是著名的武王伐纣,
+姬发建立了周朝,
+周朝是中国历史上最长的朝代,790年的国运,
+武王姬发有个非常出名的弟弟是孔子的偶像,叫做鸡蛋,也就是大名鼎鼎的周公。
+周公创立了宗法制平定三间之乱,辅佐少年天子。
+周朝传了好多代到了周幽王,
+周幽王为博妖姬包四一笑,发生了著名的烽火戏诸侯,这还不够,周幽王还废了皇后和太子,要立包四的儿子为太子,老丈人直接就火了。
+于是联合犬戎直接攻破吴京,也就是西安杀了周幽王,
+都城西安也被犬戎一把火给烧为灰烬,然后周平王即位,宫殿被烧也不修了,直接就迁都了。
+在各位诸侯的帮助下,把首都从西安搬到了洛阳,
+这就是著名的平王东迁,
+周天子要钱没钱,要权没权,地位从此一落千丈。
+平王东迁以前叫做西周平,王东迁以后叫做东周,而东周又分为两个时期,前半段叫春秋,后半段叫战国,
+春秋时期出了五个霸主,齐桓公、晋文公、宋襄公、秦穆公和楚庄王,
+时称春秋五霸。
+春秋末期发生了两件大事,也是春秋和战国的分界点三家分晋和田氏代旗。
+战国时期的战国七雄、秦、楚、齐、燕、赵、魏、韩在经过一顿火拼之后,最终秦国突出重围灭了六国。
+秦始皇嬴政一统天下,建立秦朝,迎来了中国历史上第一次民族融合的高峰。
+秦始皇死后,由于之前的统治很残暴,秦二世屁股还没坐热,就爆发了中国历史上第一次大规模的农民起义,陈胜、吴广的大泽乡起义,
+虽然以失败告终,但是星星之火已成燎原之势。
+刘邦项羽合作灭了秦国后,两大势力又开始了楚汉争霸,最终该下一战项羽四面楚歌,全军覆没。
+刘邦建立了汉朝,史称西汉,到了西汉第七位皇帝汉武帝刘彻开创了汉武盛世,
+维西汉最鼎盛的时期,
+张骞出使西域也是这段时间的事儿。
+西汉存在了200多年后被权臣王莽所篡改,国号为新,史称新莽。
+十几年后老刘家的子孙刘秀杀回来,推翻了新朝,重建大汉王朝,
+为汉光武帝,史称东汉。
+东汉统治了将近200年后,太监的权力达到极致,其中12个太监被称为时常事,
+爆发了十常氏之乱,祸乱朝政,
+进而导致军阀董卓带兵入京,爆发了董卓之乱。
+各路诸侯一看形势不对,呀带兵就撤了,跑回自己的地盘发展。
+各个军阀再经过发展,最终通过三大战役逐渐形成三个国家。
+魏蜀、吴
+三国平衡发展了40年后,魏国灭了蜀国,
+可是没几年魏国权臣司马懿的孙子司马炎就篡位了,
+把国号魏改成了晋,史称西晋,紧接着西晋就灭了吴国,统一全国西晋的第二位皇帝,就是历史上著名的白痴皇帝司马衷,
+他有个巨丑的老婆叫贾南风,就是这个贾南风成为了西晋八王之乱的导火索。
+这时候北方游牧民族趁乱崛起,并开始入侵西进,也就是五胡乱华,
+五湖中最大的是匈奴
+首领刘渊建立汉赵后,他的儿子刘聪几年之内就灭了西晋,
+西晋皇族司马睿跑到了南京重建晋朝,史称东晋,这就是历史上的衣冠南渡。
+而北方这些胡人逐渐形成了16个割据政权,史称五胡十六国和南方的东晋政权对峙。
+这时候五湖中的意志崛起了,就是底租富家。
+福建建立了前秦政权,一统北方自称大秦天王,
+然后福建就不老实了,就开始琢磨东晋了,率30万大军南下,在淝水之战中被东晋、谢贤、谢安、谢石等人率8万人直接摁倒在了肥水。
+北方各势力又脱离了前秦的统治,各自独立再次混乱。
+其中又有一股势力崛起了,就是鲜卑族的拓跋家族见过魏始昌北魏。
+几十年后北魏统一了北方,而东晋政权也被东晋权臣刘裕所篡,
+建国为宋,史称南朝宋,东晋灭亡,南北朝开始。
+南朝依次为宋齐良辰4个朝代,共计169年。
+注意这是顺序出现的4个朝代,
+而北朝北魏分裂为西魏和东魏各自用力自己的皇帝互相各种不服。
+后来东魏、西魏又都各自干掉了自己的皇帝,东魏变成北齐,
+西魏变成北周。
+20多年后,北周皇帝周武帝宇文邕率领亲家杨坚灭了北齐统一北方。
+北周武帝死后几年之内,杨坚就废了自己的外孙篡位称帝,建国为隋,
+7年后又灭了南朝的陈朝统一全国。
+隋朝第二位皇帝隋炀帝杨广是个演员,通过在父亲杨坚母亲独孤迦罗面前的表演,让太子杨勇被废,并顺利成为了太子,并继承了王位。
+在位14年建造了东都洛阳、隋朝大运河,
+三下扬州三争高沟里严重削弱了国力,民不聊生,爆发了农民起义。
+最终隋朝被隋炀帝杨广的表哥李渊建立的唐朝所灭,
+李渊的这几个儿子为了皇位又干起来了,
+最终李世民在玄武门之变中射死了,太子李建成,又让老爹李渊禅让皇位,与他成为唐太宗,并开启了著名的贞观之治。
+这时候李世民身边出来一位姑娘叫武兆,也就是后来的武则天,
+反正李世民是没看上他,武昭12年的地位一直没有提升,
+吴钊一看实在是不能在李世民身上下手了,
+再不努力就老了。
+就在李世民的儿子李治身上打起了主意,成功成为李治的皇后。
+最终武兆废了儿子李显自己称帝,改唐为周示好则天皇后。
+15年后,武则天的儿子李显在弟弟李旦和妹妹太平公主的共同主持下,发动了神龙政变,
+推翻了老娘武则天。
+李显第二次登机,
+结果李显的老婆韦皇后竟然要成为第二个武则天,
+并且毒死了李显,而且要谋害李诞。
+李诞在儿子李隆基和妹妹太平公主的协助下,发动唐隆政变,灭了韦皇后集团李诞登基。
+两年后禅让儿子李隆基,
+李隆基开创了开元盛世,为唐朝的极盛时期,李隆基志得意满开始不务正业了。
+在娶了儿子李瑁的老婆杨玉环后,更加沉溺酒色,
+爆发了安史之乱,唐朝国力大衰,
+最终唐朝权臣朱温通过禅让的形式夺取了唐哀帝的地位,建国为梁,史称后梁。
+唐朝灭亡5代10国开始共72年,中原地区顺序存在了5个朝代,
+及梁、唐、晋、汉、周除中原5代以外,同时还存在10个独立的小国家及十国。
+5代与10国处于同一时期,区别就是5代是一个一个存在的,而10国是同时存在的。
+5代中最后一个朝代,后周大将军赵匡胤发动陈桥兵变,黄袍加身,篡后周政权,
+建立北宋至此5代10国结束。
+赵匡胤把这十国零零碎碎的灭了以后,基本统一了全国,形成了和契丹人建立的辽国对峙的局面。
+在北宋和契丹人建立的辽国对峙的时候,辽国后方的女真族建立的金国崛起了,
+辽国正好夹在北宋和金国之间,于是北宋就联合金国把辽国给灭了,
+但是金国带着余威直接就灭了北宋,
+一个北宋的皇子就跑到了现在的河南商丘,建立了南宋南宋在岳飞带领的岳家军的铁骑下抵抗金国,
+但是岳飞最终被宋高宗赵构和秦桧以莫须有的罪名所杀。
+这个时候北方蒙古大草原又崛起了一股势力,就是后来的元朝的建立者蒙古国,
+蒙古国直接联合南宋灭了金国。
+1271年忽必烈在现在的北京建立元朝,攻打南宋两年后,南宋丞相陆秀夫背着年仅8岁的小皇帝跳海自尽,
+贵族800人和10万军民跳海殉国,南宋灭亡。
+元朝统治者变本加厉的向汉人收取各种名目繁多的赋税,民族压迫严重,
+人民揭竿而起。
+元朝统治了98年后,被一个叫朱重八的和尚,也就是朱元璋在大将徐达常遇春的协助下灭了元朝。
+明朝建立明太祖朱元璋通过胡兰之玉为皇太孙朱允文铺平了道路,
+没想到朱允文被自己的叔叔朱棣在黑袍妖僧、姚广孝的寸头下给灭了。
+朱棣把都城由南京迁到了自己的根据地,北京。
+明朝经历了明末农民起义爆发,
+关外爱新觉罗努尔哈赤建立的后金政权趁势崛起,国家处于内忧外患的境地。
+1644年李自成起义军攻克北京建国大顺,崇祯自缢于梅山,
+当天下很多人认为李自成的大顺政权将是下一个朝代之时,
+结果山海关一战以后,李自成败于吴三桂与清军联手,大顺政权也随着他的死亡而灭亡,
+大顺没有彻底的消灭南明朝廷统一中国,所以只能是明朝与清朝之间的过渡政权。
+清朝入关后,摄政王多尔衮颁布剃发令,并对拒不执行的汉人进行了大屠杀,
+因剃发令而被满清杀死的汉人不下几十万。
+康熙、雍正、乾隆盛世是中国古代封建王朝的最后一个盛世。
+清朝中后期,清朝以天朝上邦自居,闭关锁国导致外国贸易逆差,
+发生了第一次和第二次鸦片战争,清朝彻底沦为半殖民地半封建社会。
+1894年中日甲午战争清朝惨败,
+其中日军对旅顺进行了惨绝人寰的大屠杀,
+这也让其他各国对清朝这块大蛋糕虎视眈眈。
+1900年八国联军入侵慈禧,光绪跑路,李鸿章代表清朝签订了赔款数目最大,主权丧失最严重的不平等条约,
+辛丑条约。
+1911年到1912年爆发了成功推翻清朝统治的联唱革命运动辛亥革命?
+1912年孙文以绝对优势当选为中华民国第一任临时大总统,也正式宣告中华民国的成立。
+孙中山以清朝内阁大臣袁世凯支持共和体制为要求,辞去了临时大总统让位给袁世凯,
+袁世凯迫使宣统帝溥仪退位,清朝灭亡。
+袁世凯成为总统后妄图称帝,并于1915年登基自称皇帝,
+引发了护国运动。
+南方各省纷纷宣布独立并出兵讨伐袁世凯,袁世凯迫于内外压力,不得不在做了83天皇帝后取消了帝制,
+随后病死于尿毒症。
+袁世凯死后手下北洋军阀三大派系失去了控制,开始混战。
+1931年918事变到1945年二战后,日本宣布无条件投降,
+中国人民14年的抗日战争以胜利告终。
+1948年到1949年辽沈战役、淮海战役、平津战役三大战役后,
+蒋介石飞往台北。
+1949年10月1日中华人民共和国成立,北平改名北京。
+我我弟弟。
+呀"""
+#     text = """艾叶加上酒全身疼痛,不再有比止疼药都管用,大家好,我是中医马大夫,
+# 艾叶加上酒疼痛不再有春天到了,
+# 颈椎病、肩周炎、腰疼、膝关节炎,还有老寒腿,
+# 都会时不时发作,
+# 很多的人都会随意的吃点止疼药,
+# 其实很多人都不懂,一吃就好的止疼药副作用是非常大的。
+# 这里告诉大家一个超级简单的方法,
+# 艾叶加上酒专治颈椎病、肩周炎、腰腿疼痛,
+# 老寒腿,
+# 具体怎么做,呢今天这个视频呢给大家讲清楚,
+# 建议大家点赞收藏并转发,让更多的人受益。
+# 我们先取艾叶60克,
+# 生姜是5颗,
+# 大葱2~3颗,
+# 白酒适量将三味药捣烂,用纱布包好,
+# 浇上热酒,
+# 扶到我们疼痛处,
+# 一般患者热敷上1~2次就不会再疼了,
+# 简单有效,
+# 比止疼药都管用,适用于风寒湿引起的疼痛都有效果。
+# 关注我每天晚上直播讲解健康知识,再见。
+# 家人们这么有用的健康知识,看完记得转发收藏起来,身体是最重要的。
+# 多少朋友因为被病痛折磨,很多人随意吃药,这个中医大师的方法
+# 赶紧分享给大家,这个方法很简单,还有很多朋友不知道赶紧转发到各大群里,
+# 让老友们都知道,说不定就用上了,转发到群里和群友共享,他们会感激您的。"""
+    print(len(text))
+    get_tag(text=text)

+ 40 - 0
log.py

@@ -0,0 +1,40 @@
+import logging
+import logging.config
+
+from log_conf import conf
+
+
+class Log(object):
+    def __init__(self):
+        # 配置
+        logging.config.dictConfig(conf)
+
+    def __console(self, level, message):
+        if level == 'info':
+            logger = logging.getLogger('sls')
+            logger.info(message)
+        elif level == 'debug':
+            logger = logging.getLogger('root')
+            logger.debug(message)
+        elif level == 'warning':
+            logger = logging.getLogger('root')
+            logger.warning(message)
+        elif level == 'error':
+            logger = logging.getLogger('error')
+            logger.error(message)
+
+    def debug(self, message):
+        self.__console('debug', message)
+        # return
+
+    def info(self, message):
+        self.__console('info', message)
+        # return
+
+    def warning(self, message):
+        self.__console('warning', message)
+        # return
+
+    def error(self, message):
+        self.__console('error', message)
+        # return

+ 87 - 0
log_conf.py

@@ -0,0 +1,87 @@
+# log conf
+import logging
+import aliyun
+import os
+import time
+from config import set_config
+config_ = set_config()
+
+# 本地日志存储路径
+log_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "logs")
+if not os.path.exists(log_path):
+    os.makedirs(log_path)
+# 文件的命名
+log_name = os.path.join(log_path, '{}.log'.format(time.strftime('%Y%m%d')))
+
+conf = {
+    'version': 1,
+    'formatters': {
+        'rawFormatter': {
+            'class': 'logging.Formatter',
+            'format': '%(message)s'
+        },
+        'simpleFormatter': {
+            'class': 'logging.Formatter',
+            'format': '%(asctime)s %(levelname)s: %(message)s'
+        }
+    },
+    'handlers': {
+        'consoleHandler': {
+            '()': 'logging.StreamHandler',
+            'level': 'DEBUG',
+            'formatter': 'simpleFormatter',
+        },
+        # 'slsHandler': {
+        #     '()': 'aliyun.log.QueuedLogHandler',
+        #     'level': 'INFO',
+        #     'formatter': 'rawFormatter',
+        #     # custom args:
+        #     'end_point': config_.ALIYUN_LOG.get('ENDPOINT', ''),
+        #     'access_key_id': config_.ALIYUN_LOG.get('ACCESSID', ''),
+        #     'access_key': config_.ALIYUN_LOG.get('ACCESSKEY', ''),
+        #     'project': config_.ALIYUN_LOG.get('PROJECT', ''),
+        #     'log_store': "info",
+        #     'extract_kv': True,
+        #     'extract_json': True
+        # },
+        # 'errorHandler': {
+        #     '()': 'aliyun.log.QueuedLogHandler',
+        #     'level': 'ERROR',
+        #     'formatter': 'rawFormatter',
+        #     # custom args:
+        #     'end_point': config_.ALIYUN_LOG.get('ENDPOINT', ''),
+        #     'access_key_id': config_.ALIYUN_LOG.get('ACCESSID', ''),
+        #     'access_key': config_.ALIYUN_LOG.get('ACCESSKEY', ''),
+        #     'project': config_.ALIYUN_LOG.get('PROJECT', ''),
+        #     'log_store': "error",
+        #     'extract_kv': True,
+        #     'extract_json': True
+        # },
+        'fileHandler': {
+            '()': 'logging.FileHandler',
+            'level': 'INFO',
+            'formatter': 'simpleFormatter',
+            'filename': log_name,
+            'mode': 'a',
+            'encoding': 'utf-8'
+        }
+    },
+    'loggers': {
+        'root': {
+            'handlers': ['consoleHandler', ],
+            'level': 'DEBUG'
+        },
+        'sls': {
+            # 'handlers': ['consoleHandler', 'slsHandler'],
+            'handlers': ['consoleHandler', 'fileHandler'],
+            'level': 'INFO',
+            'propagate': False
+        },
+        'error': {
+            # 'handlers': ['consoleHandler', 'errorHandler'],
+            'handlers': ['consoleHandler', 'fileHandler'],
+            'level': 'ERROR',
+            'propagate': False
+        }
+    }
+}

+ 171 - 0
main_process.py

@@ -0,0 +1,171 @@
+import os.path
+import traceback
+
+import requests
+from feishu import FeiShuHelper
+from audio_process import get_wav
+from xunfei_asr import RequestApi
+from gpt_tag import get_tag
+from config import set_config
+from log import Log
+config_ = set_config()
+log_ = Log()
+
+
+def download_video(video_url, video_id, download_folder, ftype='mp4'):
+    if not os.path.exists(download_folder):
+        os.makedirs(download_folder)
+    response = requests.get(video_url, stream=True)
+    if response.status_code == 200:
+        filename = f"{download_folder}/{video_id}.{ftype}"
+        with open(filename, "wb") as video_file:
+            for chunk in response.iter_content(chunk_size=8192):
+                video_file.write(chunk)
+        return filename
+
+
+def call_asr(audio_path):
+    api = RequestApi(appid=config_.XFASR_CONFIG['appid'],
+                     secret_key=config_.XFASR_CONFIG['secret_key'],
+                     upload_file_path=audio_path)
+    order_id = api.upload()
+    result = api.get_result(order_id)
+    asr_res = api.parse_lattice(result)
+    dialogue_path = audio_path.replace('.wav', '.txt')
+    with open(dialogue_path, 'w') as f:
+        f.write(asr_res)
+    return asr_res
+
+
+def main(sheet_info_config):
+    video_spreadsheet_token = sheet_info_config['video_spreadsheet_token']
+    video_sheet_id = sheet_info_config['video_sheet_id']
+    read_start_row = sheet_info_config['read_start_row']
+    res_spreadsheet_token = sheet_info_config['res_spreadsheet_token']
+    res_sheet_id = sheet_info_config['res_sheet_id']
+    write_start_row = sheet_info_config['write_start_row']
+    write_start_col = sheet_info_config['write_start_col']
+    write_end_col = sheet_info_config['write_end_col']
+
+    # 1. 读取飞书表格,获取视频url和videoId
+    feishu_helper = FeiShuHelper()
+    data = feishu_helper.get_data(spreadsheet_token=video_spreadsheet_token, sheet_id=video_sheet_id)
+    videos = []
+    for item in data[read_start_row:read_start_row+100]:
+        if video_sheet_id == 'nz1pRo':
+            videos.append(
+                {
+                    'videoId': item[1],
+                    'url': item[2][0]['text'],
+                    'title': item[6]
+                }
+            )
+        elif video_sheet_id == '3ba53c':
+            videos.append(
+                {
+                    'videoId': item[0],
+                    'url': item[1][0]['text']
+                }
+            )
+    log_.info(f"videos count: {len(videos)}")
+
+    result = []
+    for i, video in enumerate(videos):
+        try:
+            log_.info(f"i = {i}, video = {video}")
+            # 2. 下载视频
+            video_id = video['videoId']
+            video_url = video['url']
+            video_path = download_video(video_url=video_url, video_id=video_id, download_folder='videos')
+            print(video_path)
+            log_.info(f"video_path = {video_path}")
+            # 3. 获取视频中的音频
+            audio_path = get_wav(video_path=video_path)
+            print(audio_path)
+            log_.info(f"audio_path = {audio_path}")
+            # 4. asr
+            asr_res = call_asr(audio_path=audio_path)
+            print(asr_res)
+            log_.info(f"asr_res = {asr_res}")
+            # 5. gpt产出结果
+            gpt_res = get_tag(text=asr_res)
+            print(gpt_res)
+            log_.info(f"gpt_res = {gpt_res}")
+
+            if video_sheet_id == 'nz1pRo':
+                result = [[video_id, video_url, video['title'], asr_res, gpt_res]]
+            elif video_sheet_id == '3ba53c':
+                result = [[video_id, video_url, asr_res, gpt_res]]
+            log_.info(f"result = {result}")
+            # 6. 结果写入飞书表格
+            if len(result) > 0:
+                feishu_helper.data_to_feishu_sheet(
+                    sheet_token=res_spreadsheet_token,
+                    sheet_id=res_sheet_id,
+                    data=result,
+                    start_row=write_start_row,
+                    start_column=write_start_col,
+                    end_column=write_end_col
+                )
+                log_.info(f"write to feishu success!")
+                write_start_row += 1
+        except Exception as e:
+            log_.error(e)
+            log_.error(traceback.format_exc())
+            continue
+
+    # 6. 结果写入飞书表格
+    # if len(result) > 0:
+    #     feishu_helper.data_to_feishu_sheet(
+    #         sheet_token=res_spreadsheet_token,
+    #         sheet_id=res_sheet_id,
+    #         data=result,
+    #         start_row=write_start_row,
+    #         start_column=write_start_col,
+    #         end_column=write_end_col
+    #     )
+
+
+if __name__ == '__main__':
+    # sheet_info = {
+    #     '每日标题审核记录': {
+    #         'video_spreadsheet_token': 'shtcn1fmHJ2z0oc3j9OScBOlAbe',
+    #         'video_sheet_id': 'nz1pRo',
+    #         'read_start_row': 1,
+    #         'res_spreadsheet_token': 'DkiUsqwJ6hmBxstBYyEcNE4ante',
+    #         'res_sheet_id': '08d4cc',
+    #         'write_start_row': 2,
+    #         'write_start_col': 'A',
+    #         'write_end_col': 'E'
+    #     },
+    #     'top 视频需要识别内容主题等信息': {
+    #         'video_spreadsheet_token': 'shtcndUUt61ItHYp8C8goBp7Sah',
+    #         'video_sheet_id': '3ba53c',
+    #         'read_start_row': 1,
+    #         'res_spreadsheet_token': 'DkiUsqwJ6hmBxstBYyEcNE4ante',
+    #         'res_sheet_id': 'LErgi2',
+    #         'write_start_row': 2,
+    #         'write_start_col': 'A',
+    #         'write_end_col': 'D'
+    #     }
+    # }
+    #
+    # for sheet_tag, sheet_item in sheet_info.items():
+    #     print(sheet_tag)
+    #     main(sheet_info_config=sheet_item)
+
+    video_path = download_video(
+        video_url='http://rescdn.yishihui.com/longvideo/video/vpc/20230420/22421791F3yZJNHSelDuvs04zd',
+        video_id='001', download_folder='videos', ftype='mp4')
+    print(video_path)
+    # 3. 获取视频中的音频
+    audio_path = get_wav(video_path=video_path)
+    print(audio_path)
+    log_.info(f"audio_path = {audio_path}")
+    # 4. asr
+    asr_res = call_asr(audio_path=audio_path)
+    print(asr_res)
+    log_.info(f"asr_res = {asr_res}")
+    # 5. gpt产出结果
+    gpt_res = get_tag(text=asr_res)
+    print(gpt_res)

+ 80 - 0
temporary_process.py

@@ -0,0 +1,80 @@
+import traceback
+
+from feishu import FeiShuHelper
+from gpt_tag import get_tag
+from config import set_config
+from log import Log
+config_ = set_config()
+log_ = Log()
+
+
+def main(sheet_info_config):
+    text_spreadsheet_token = sheet_info_config['text_spreadsheet_token']
+    text_sheet_id = sheet_info_config['text_sheet_id']
+    read_start_row = sheet_info_config['read_start_row']
+    text_index = sheet_info_config['text_index']
+    res_spreadsheet_token = sheet_info_config['res_spreadsheet_token']
+    res_sheet_id = sheet_info_config['res_sheet_id']
+    write_start_row = sheet_info_config['write_start_row']
+    write_start_col = sheet_info_config['write_start_col']
+    write_end_col = sheet_info_config['write_end_col']
+
+    # 1. 读取飞书表格,获取视频asr_res
+    feishu_helper = FeiShuHelper()
+    data = feishu_helper.get_data(spreadsheet_token=text_spreadsheet_token, sheet_id=text_sheet_id)
+    for i, item in enumerate(data[read_start_row:]):
+        try:
+            log_.info(f"i: {i}, videoId: {item[0]}")
+            # print(item)
+            text = item[text_index]
+            # print(text)
+            # 5. gpt产出结果
+            gpt_res = get_tag(text=text)
+            print(gpt_res)
+            log_.info(f"gpt_res = {gpt_res}")
+            # 6. 结果写入飞书表格
+            log_.info(f"start_row: {write_start_row + i}")
+            feishu_helper.update_values(
+                sheet_token=res_spreadsheet_token,
+                sheet_id=res_sheet_id,
+                data=[[gpt_res]],
+                start_row=write_start_row+i,
+                start_column=write_start_col,
+                end_column=write_end_col
+            )
+            log_.info(f"write to feishu success!")
+        except Exception as e:
+            log_.error(e)
+            log_.error(traceback.format_exc())
+            continue
+
+
+if __name__ == '__main__':
+    sheet_info = {
+        '每日标题审核记录': {
+            'text_spreadsheet_token': 'DkiUsqwJ6hmBxstBYyEcNE4ante',
+            'text_sheet_id': '08d4cc',
+            'read_start_row': 1,
+            'res_spreadsheet_token': 'DkiUsqwJ6hmBxstBYyEcNE4ante',
+            'res_sheet_id': '08d4cc',
+            'write_start_row': 2,
+            'write_start_col': 'I',
+            'write_end_col': 'I',
+            'text_index': 3
+        },
+        'top 视频需要识别内容主题等信息': {
+            'text_spreadsheet_token': 'DkiUsqwJ6hmBxstBYyEcNE4ante',
+            'text_sheet_id': 'LErgi2',
+            'read_start_row': 1,
+            'res_spreadsheet_token': 'DkiUsqwJ6hmBxstBYyEcNE4ante',
+            'res_sheet_id': 'LErgi2',
+            'write_start_row': 2,
+            'write_start_col': 'H',
+            'write_end_col': 'H',
+            'text_index': 2
+        }
+    }
+
+    for sheet_tag, sheet_item in sheet_info.items():
+        print(sheet_tag)
+        main(sheet_info_config=sheet_item)

+ 66 - 0
utils.py

@@ -0,0 +1,66 @@
+import requests
+import os
+import json
+import traceback
+from log import Log
+from config import set_config
+log_ = Log()
+config_ = set_config()
+
+
+def request_post(request_url, headers, request_data):
+    """
+    post 请求 HTTP接口
+    :param request_url: 接口URL
+    :param headers: 请求头
+    :param request_data: 请求参数
+    :return: res_data json格式
+    """
+    try:
+        response = requests.post(url=request_url, json=request_data, headers=headers)
+        # print(response)
+        if response.status_code == 200:
+            res_data = json.loads(response.text)
+            return res_data
+        else:
+            return None
+    except Exception as e:
+        log_.error('url: {}, exception: {}, traceback: {}'.format(request_url, e, traceback.format_exc()))
+        return None
+
+
+def request_get(request_url, headers, params=None):
+    """
+    get 请求 HTTP接口
+    :param request_url: 接口URL
+    :param headers: 请求头
+    :param params: 请求参数
+    :return: res_data json格式
+    """
+    try:
+        response = requests.get(url=request_url, headers=headers, params=params)
+        if response.status_code == 200:
+            res_data = json.loads(response.text)
+            return res_data
+        else:
+            return None
+    except Exception as e:
+        log_.error('url: {}, exception: {}, traceback: {}'.format(request_url, e, traceback.format_exc()))
+        return None
+
+
+def download_video(video_path, video_id, download_folder, ftype='mp4'):
+    """下载视频"""
+    if not os.path.exists(download_folder):
+        os.makedirs(download_folder)
+    filename = f"{download_folder}/{video_id}.{ftype}"
+    # 视频已存在,则不重复下载
+    if os.path.exists(filename):
+        return filename
+    response = requests.get(video_path, stream=True)
+    if response.status_code == 200:
+        with open(filename, "wb") as video_file:
+            for chunk in response.iter_content(chunk_size=8192):
+                video_file.write(chunk)
+        return filename
+

+ 141 - 0
xunfei_asr.py

@@ -0,0 +1,141 @@
+import ast
+import base64
+import hashlib
+import hmac
+import json
+import time
+import requests
+import urllib
+import os
+from audio_process import get_audio_duration
+from config import set_config
+
+config_ = set_config()
+
+
+class RequestApi(object):
+    def __init__(self, appid, secret_key, upload_file_path):
+        self.appid = appid
+        self.secret_key = secret_key
+        self.upload_file_path = upload_file_path
+        self.ts = str(int(time.time()))
+        self.signa = self.get_signa()
+
+    def get_signa(self):
+        """
+        signa生成
+        :return: signa
+        """
+        # signa的生成公式:HmacSHA1(MD5(appid + ts),secretkey)
+        m2 = hashlib.md5()
+        m2.update((self.appid + self.ts).encode('utf-8'))
+        md5 = m2.hexdigest()
+        md5 = bytes(md5, encoding='utf-8')
+        # 以secret_key为key, 上面的md5为msg, 使用hashlib.sha1加密结果为signa
+        signa = hmac.new(self.secret_key.encode('utf-8'), md5, hashlib.sha1).digest()
+        signa = base64.b64encode(signa)
+        signa = str(signa, 'utf-8')
+        return signa
+
+    def upload(self):
+        """
+        上传
+        :return: orderId
+        """
+        # 获取音频文件大小
+        file_len = os.path.getsize(self.upload_file_path)
+        file_name = os.path.basename(self.upload_file_path)
+        # 获取音频时长
+        duration = get_audio_duration(self.upload_file_path)
+        # 请求参数拼接
+        param_dict = {
+            'appId': self.appid,
+            'signa': self.signa,
+            'ts': self.ts,
+            'fileSize': file_len,
+            'fileName': file_name,
+            'duration': str(duration),
+            'roleType': 1
+        }
+        # print("upload参数:", param_dict)
+        # 以二进制方式读取音频文件内容
+        data = open(self.upload_file_path, 'rb').read(file_len)
+        # 请求upload api
+        response = requests.post(
+            url=config_.XFASR_HOST + config_.XF_API['upload'] + "?" + urllib.parse.urlencode(param_dict),
+            headers={"Content-type": "application/json"},
+            data=data
+        )
+        # print("upload_url:", response.request.url)
+        result = json.loads(response.text)
+        # print("upload resp:", result)
+        return result['content']['orderId']
+
+    def get_result(self, order_id):
+        """
+        查询结果
+        :param order_id:
+        :return: result
+        """
+        param_dict = {
+            'appId': self.appid,
+            'signa': self.signa,
+            'ts': self.ts,
+            'orderId': order_id,
+            'resultType': 'transfer'
+        }
+        status = 3
+        # 建议使用回调的方式查询结果,查询接口有请求频率限制
+        while status == 3:
+            response = requests.post(
+                url=config_.XFASR_HOST + config_.XF_API['get_result'] + "?" + urllib.parse.urlencode(param_dict),
+                headers={"Content-type": "application/json"}
+            )
+            # print("get_result_url:",response.request.url)
+            result = json.loads(response.text)
+            status = result['content']['orderInfo']['status']
+            if status == 4:
+                return result
+            time.sleep(5)
+
+    def parse_lattice(self, result):
+        content = result['content']['orderResult']
+        content = ast.literal_eval(content)
+        contents = content['lattice']
+        asr_ret = ''
+        for js in contents:
+            json_1best = js['json_1best']
+            json_1best = ast.literal_eval(json_1best)
+            # print(json_1best)
+            json_1best_contents = json_1best['st']['rt']
+            l = []
+            for cw in json_1best_contents:
+                cws = cw['ws']
+                for cw in cws:
+                    l.append(cw['cw'][0]['w'])
+            asr_ret += ''.join(l)+'\n'
+        return asr_ret
+
+
+def call_asr(audio_path):
+    """ASR"""
+    dialogue_path = audio_path.replace('.wav', '.txt')
+    # 视频已识别,则不重复调用,直接读取文件中的内容
+    if os.path.exists(audio_path):
+        with open(dialogue_path, 'r') as rf:
+            asr_res = ''.join(rf.readlines())
+    else:
+        api = RequestApi(appid=config_.XFASR_CONFIG['appid'],
+                         secret_key=config_.XFASR_CONFIG['secret_key'],
+                         upload_file_path=audio_path)
+        order_id = api.upload()
+        result = api.get_result(order_id)
+        asr_res = api.parse_lattice(result)
+        with open(dialogue_path, 'w') as f:
+            f.write(asr_res)
+    return dialogue_path, asr_res
+
+
+if __name__ == '__main__':
+    audio_path = 'videos/001.wav'
+    call_asr(audio_path=audio_path)