This commit is contained in:
qcloud
2025-05-15 15:26:06 +08:00
parent 330976812d
commit e29f79b9ac
2804 changed files with 1044973 additions and 83 deletions

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from flask import request , Response, jsonify,stream_with_context
from flask import request , Response, jsonify,stream_with_context,send_file
from api import settings
from api.db import LLMType
from api.db import StatusEnum
@@ -24,16 +24,60 @@ from api.db.services.user_service import TenantService
from api.db.services.brief_service import MesumOverviewService
from api.db.services.llm_service import LLMBundle
from api.db.services.antique_service import MesumAntiqueService
from api.db.services.appinfo_service import AppInfoService
from api.utils import get_uuid
from api.utils.api_utils import get_error_data_result, token_required
from api.utils.api_utils import get_result
from api.utils.file_utils import get_project_base_directory
from rag.utils.minio_conn import RAGFlowMinio
import logging
import base64, gzip
import io, re, json
from io import BytesIO
import queue,time,uuid,os,array
from threading import Lock,Thread
from zhipuai import ZhipuAI
from openai import OpenAI
import openai
from datetime import datetime
# 为APP升级 cyx 2025-04-20
@manager.route('/get_apk_update/<version>', methods=['GET'])
@token_required
def get_apk_update_info(tenant_id,version):
try:
# 正则表达式匹配版本号格式
pattern = r'^\d+(\.\d+)*$'
if bool(re.match(pattern, version)) is False:
version = "1.0.0"
front_app_version_int = int(version.replace('.', '')) # 前端发起来的版本号(数值)
res = []
app_infos = AppInfoService.get_all()
for o in app_infos:
val = o.to_dict()
try:
if val.get("app_version"):
app_version_int = int(val.get("app_version").replace('.', '')) # 将类似1.0.4 转换为104
if app_version_int >= front_app_version_int:
del val['create_time']
del val['create_date']
del val['update_time']
del val['update_date']
del val['description']
try:
# 转换upload_time 格式
# 将 datetime 对象格式化为 "yyyy-mm-dd hh:mm:ss" 格式
val['upload_time'] = val.get('upload_time').strftime("%Y-%m-%d %H:%M:%S")
finally:
pass
res.append(val)
finally:
pass
return get_result(data=res)
except Exception as e:
return get_error_data_result(message=f"Get apk update info error {e}")
# 用户已经添加的模型 cyx 2025-01-26
@manager.route('/get_llms', methods=['GET'])
@@ -41,6 +85,8 @@ from zhipuai import ZhipuAI
def my_llms(tenant_id):
# request.args.get("id") 通过request.args.get 获取GET 方法传入的参数
model_type = request.args.get("type")
device_id = request.args.get('device_id')
logging.info(f"get llms {model_type} {device_id}")
try:
res = {}
for o in TenantLLMService.get_my_llms(tenant_id):
@@ -50,12 +96,29 @@ def my_llms(tenant_id):
"tags": o["tags"],
"llm": []
}
if o["model_type"].lower() == 'tts': # 20250502 对tts 的模型进行特殊处理,只返回说明中注明成年 和 童声各1个
pattern = r"\([^)]*\)" # 匹配非嵌套的简单括号内容 (童声) (成年)
matches = re.findall(pattern, o['description'])
if len(matches) == 1:
voice_type = "adult"
if "" in matches[0]:
voice_type = "child"
res[o["llm_factory"]]["llm"].append({
"type": o["model_type"],
"name": o["llm_name"],
"used_token": o["used_tokens"],
"description": o["description"],
"voice_type": voice_type
})
else:
res[o["llm_factory"]]["llm"].append({
"type": o["model_type"],
"name": o["llm_name"],
"used_token": o["used_tokens"],
"description":o["description"]
})
res[o["llm_factory"]]["llm"].append({
"type": o["model_type"],
"name": o["llm_name"],
"used_token": o["used_tokens"]
})
return get_result(data=res)
except Exception as e:
return get_error_data_result(message=f"Get LLMS error {e}")
@@ -69,28 +132,22 @@ def upload_file(tenant_id,mesum_id):
return jsonify({'error': 'No file part'}), 400
antiques_selected = ""
if mesum_id:
"""
e,mesum_breif = MesumOverviewService.get_by_id(mesum_id)
if not e:
logging.info(f"没有找到匹配的博物馆信息,mesum_id={mesum_id}")
else:
antiques_selected =f"结果从:{mesum_breif.antique} 中进行选择"
"""
mesum_id_str = str(mesum_id)
antique_labels=get_antique_labels(mesum_id)
# 使用列表推导式和str()函数将所有元素转换为字符串
string_elements = [str(element) for element in antique_labels]
# 使用join()方法将字符串元素连接起来,以逗号为分隔符
joined_string = ','.join(string_elements)
antiques_selected = f"结果从:{joined_string} 中进行选择"
logging.info(f"{mesum_id} {joined_string}")
prompt = (f"你是一名资深的博物馆知识和文物讲解专家,同时也是一名历史学家,"
f"请识别这个图片中文字,重点识别出含在文字中的某一文物标题、某一个历史事件或某一历史人物,"
f"你的回答有2个结果第一个结果是是从文字中识别出历史文物、历史事件、历史人物,"
f"此回答时只给出匹配的文物、事件、人物,不需要其他多余的文字,{antiques_selected}"
f",第二个结果是原始识别的所有文字"
"2个结果输出以{ }的json格式给出匹配文物、事件、人物的键值为antique如果有多个请加序号如:antique1,antique2,"
mesum_id_str = str(mesum_id)
labels_with_id = get_labels_with_id(mesum_id)
antique_labels = ','.join([item['label'] for item in labels_with_id])
joined_string = antique_labels
antiques_selected = f"{joined_string}"
#logging.info(f"mesumid={mesum_id} {joined_string}")
prompt = (f"你是一名图片识别和理解助手"
f"任务是先识别图片中文字,然后理解文字中包含的内容,分析哪一项可以作为识别出文字的标题,"
f"你的回答有3个结果第一个结果匹配出的结果,JSON键值为antique"
f"从下面的候选项:{antiques_selected}进行匹配,每一个候选项中间以','分割,如果没有任何匹配则结果为'',以免误触发讲解,匹配成功则输出匹配出的内容"
f",第二个结果是原始识别的所有文字,json 键值为text"
f"第三个结果是识别出文字与匹配项列表中元素的匹配度范围从0-1,1表示100%匹配,0表示完全不匹配,JSON键值为match_score,"
"3个结果输出以{ }的json格式给出匹配出文物、事件、人物的结果键值为antique"
f"原始数据的键值为text输出是1个完整的JSON数据不要有多余的前置和后置内容确保前端能正确解析出JSON数据")
file = request.files['file']
@@ -104,11 +161,18 @@ def upload_file(tenant_id,mesum_id):
req_antique = request.form.get('antique',None)
if req_antique is None:
req_antique = main_antiquity
logging.info(f"recevie photo file {file.filename} {file_size} 识别中....")
logging.info(f"recevie photo file {file.filename} {file_size} 识别中.... ")
# vl_model = "qwen-vl-max-latest"
vl_model = "glm-4v-plus"
"""
client = ZhipuAI(api_key="5685053e23939bf82e515f9b0a3b59be.C203PF4ExLDUJUZ3") # 填写您自己的APIKey
response = client.chat.completions.create(
model="glm-4v-plus", # 填写需要调用的模型名称
model=vl_model, # 填写需要调用的模型名称
messages=[
{
"role": "user",
"content": [{"type": "text", "text":prompt}]
},
{
"role": "user",
"content": [
@@ -120,16 +184,52 @@ def upload_file(tenant_id,mesum_id):
},
{
"type": "text",
"text": prompt
"text": "json格式"
}
]
}
]
)
"""
client = OpenAI(
api_key="sk-a47a3fb5f4a94f66bbaf713779101c75",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
response = client.chat.completions.create(
model="qwen-vl-max-latest",
messages=[
{
"role": "system",
"content": [{"type": "text", "text": prompt}],
},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{img_base}"
},
}
],
},
],
)
message = response.choices[0].message
logging.info(message.content)
return jsonify({'message': 'File uploaded successfully','text':message.content }), 200
parsed_json_res = parse_markdown_json(message.content)
parsed_json_data = {"antique": "", "text": "", "match_score": 0}
if parsed_json_res.get('success') is True:
parsed_json_data = parsed_json_res.get('data')
for item in labels_with_id:
if item['label'] == parsed_json_data.get('antique'):
parsed_json_data['id'] = item.get('id')
logging.info(f"{parsed_json_data}")
return jsonify({'message': 'File uploaded successfully','text': message.content,
'data': parsed_json_data}), 200
def allowed_file(filename):
return '.' in filename and \
@@ -210,7 +310,6 @@ def split_text_at_punctuation(text, chunk_size=100):
return chunks
def extract_text_from_markdown(markdown_text):
# 移除Markdown标题
text = re.sub(r'#\s*[^#]+', '', markdown_text)
@@ -276,10 +375,11 @@ start_background_cleaner()
@manager.route('/tts_stream/<session_id>',methods=['GET'])
def tts_stream(session_id):
session = stream_manager.sessions.get(session_id)
logging.info(f"--tts_stream {session}")
if session is None:
return get_error_data_result(message="Audio stream not found or expired.")
def generate():
count = 0;
path = os.path.join(get_project_base_directory(), "api", "apps/sdk/test.mp3")
fmp3 =open(path, 'rb')
finished_event = session['finished']
try:
while not finished_event.is_set() :
@@ -300,12 +400,9 @@ def tts_stream(session_id):
retry_count = 0 # 成功收到数据重置重试计数器
except queue.Empty:
if session['stream_format'] == "wav":
# yield encode_gzip_base64(b'\x03\x04' * 1) + "\r\n"
pass
else:
yield b'' # 保持连接
#data = fmp3.read(1024)
#yield data
pass
except Exception as e:
logging.info(f"tts streag get error2 {e} ")
@@ -319,18 +416,93 @@ def tts_stream(session_id):
# 关键响应头设置
if session['stream_format'] == "wav":
resp = Response(stream_with_context(generate()), mimetype="audio/mpeg")
else:
resp = Response(stream_with_context(generate()), mimetype="audio/wav")
else:
resp = Response(stream_with_context(generate()), mimetype="audio/mpeg")
resp.headers.add_header("Cache-Control", "no-cache")
resp.headers.add_header("Connection", "keep-alive")
resp.headers.add_header("X-Accel-Buffering", "no")
return resp
def generate_mp3_header(bitrate_kbps=128, padding=0):
# 字段定义
sync = 0b11111111111 # 同步字11位
version = 0b11 # MPEG-12位
layer = 0b01 # Layer III2位
protection = 0b0 # 无CRC1位
bitrate_index = { # 比特率索引表MPEG-1 Layer III
32: 0b0001, 40:0b0010, 48:0b0011, 56:0b0100,
64:0b0101, 80:0b0110, 96:0b0111, 112:0b1000,
128:0b1001, 160:0b1010, 192:0b1011, 224:0b1100,
256:0b1101, 320:0b1110
}[bitrate_kbps]
sampling_rate = 0b00 # 44.1kHz2位
padding_bit = padding # 填充位1位
private = 0b0 # 私有位1位
mode = 0b11 # 单声道2位
mode_ext = 0b00 # 扩展模式2位
copyright = 0b0 # 无版权1位
original = 0b0 # 非原版1位
emphasis = 0b00 # 无强调2位
# 组合为32位整数大端序
header = (
(sync << 21) |
(version << 19) |
(layer << 17) |
(protection << 16) |
(bitrate_index << 12) |
(sampling_rate << 10) |
(padding_bit << 9) |
(private << 8) |
(mode << 6) |
(mode_ext << 4) |
(copyright << 3) |
(original << 2) |
emphasis
)
# 转换为4字节二进制数据
return header.to_bytes(4, byteorder='big')
@manager.route('/chats/<chat_id>/audio/pcm_mp3', methods=['GET'])
def audio_test_mp3_stream(chat_id):
logging.info(f"--audio_test_mp3_stream--{chat_id}")
file_path = os.path.join(get_project_base_directory(), "api", "apps/sdk/test.mp3")
file_size = os.path.getsize(file_path)
# 设置Last-Modified头
last_modified = datetime(2025, 4, 17, 0, 54, 40)
def generate_test_mp3():
data_length = 0
total = 0
with open(file_path, 'rb') as fmp3:
data = fmp3.read(1024)
data_length = data_length + 1024
while data:
yield data
# time.sleep(0.5)
data = fmp3.read(1024)
data_length = data_length + 1024
if data_length > 240000:
logging.info(f"sleep 5s {data_length}")
total = total + data_length
data_length = 0
# time.sleep(2)
# print("end sleep",total
if total > 130000:
print("end")
break;
return Response(generate_test_mp3(), mimetype="audio/mpeg",headers={
'Accept-Ranges':'bytes'
})
@manager.route('/chats/<chat_id>/tts/<audio_stream_id>', methods=['GET'])
def dialog_tts_get(chat_id, audio_stream_id):
global audio_text_cache
# logging.info(f"---dialog_tts_get--0 {audio_text_cache} {audio_stream_id}")
with cache_lock:
tts_info = audio_text_cache.pop(audio_stream_id, None) # 取出即删除
tts_info = audio_text_cache.get(audio_stream_id) # 取出即删除
try:
req = tts_info
if not req:
@@ -339,6 +511,7 @@ def dialog_tts_get(chat_id, audio_stream_id):
tenant_id = req.get('tenant_id')
chat_id = req.get('chat_id')
text = req.get('text', "..")
model_name = req.get('model_name')
sample_rate = req.get('tts_sample_rate',8000) # 默认8K
stream_format = req.get('tts_stream_format','mp3')
@@ -348,12 +521,15 @@ def dialog_tts_get(chat_id, audio_stream_id):
tts_model_name = dia.tts_id
if model_name: tts_model_name = model_name
tts_mdl = LLMBundle(dia.tenant_id, LLMType.TTS, tts_model_name) # dia.tts_id)
logging.info(f"dialog_tts_get {sample_rate} {stream_format}")
# logging.info(f"dialog_tts_get {sample_rate} {stream_format} {text}")
def stream_audio():
if stream_format == 'mp3':
yield generate_mp3_header()
try:
for chunk in tts_mdl.tts(text,sample_rate=sample_rate,stream_format=stream_format):
if stream_format =='wav':
#logging.info(f"yield audio data {len(chunk)} ")
yield encode_gzip_base64(chunk) + "\r\n"
else:
yield chunk
@@ -361,22 +537,40 @@ def dialog_tts_get(chat_id, audio_stream_id):
yield ("data:" + json.dumps({"code": 500, "message": str(e),
"data": {"answer": "**ERROR**: " + str(e)}},
ensure_ascii=False)).encode('utf-8')
def generate():
data = audio_stream.read(1024)
while data:
yield data
data = audio_stream.read(1024)
logging.info("audio stream end")
try:
del audio_text_cache[audio_stream_id]
finally:
pass
if audio_stream:
# 确保流的位置在开始处
audio_stream.seek(0)
resp = Response(generate(), mimetype="audio/mpeg")
if stream_format == 'wav':
resp = Response(generate(), mimetype="audio/wav")
else:
headers = {
'Content-Type': 'audio/mpeg',
'Content-Length': str(tts_info.get('chunk_size',2048)),
'Accept-Ranges': 'bytes'
}
resp = Response(generate(),
#mimetype="audio/mpeg",
headers = headers
)
else:
if stream_format == 'wav':
resp = Response(stream_audio(), mimetype="audio/wav")
else:
resp = Response(stream_audio(), mimetype="audio/mpeg")
resp = Response(
stream_audio(),
mimetype="audio/mpeg"
)
resp.headers.add_header("Cache-Control", "no-cache")
resp.headers.add_header("Connection", "keep-alive")
resp.headers.add_header("X-Accel-Buffering", "no")
@@ -386,20 +580,22 @@ def dialog_tts_get(chat_id, audio_stream_id):
return get_error_data_result(message="音频流传输失败")
finally:
# 确保资源释放
if tts_info and tts_info.get('audio_stream') and not tts_info['audio_stream'].closed:
tts_info['audio_stream'].close()
# if tts_info and tts_info.get('audio_stream') and not tts_info['audio_stream'].closed:
# tts_info['audio_stream'].close()
pass
@manager.route('/chats/<chat_id>/tts', methods=['POST'])
@token_required
def dialog_tts_post(tenant_id, chat_id):
global audio_text_cache
req = request.json
try:
if not req.get("text"):
return get_error_data_result(message="Please input your question.")
text = req.get('text')
delay_gen_audio = req.get('delay_gen_audio', False)
model_name = req.get('model_name')
model_name = req.get('model_name') # 示例:"cosyvoice-v1/longyuan@Tongyi-Qianwen" "sambert-zhiru-v1@Tongyi-Qianwen"
audio_stream_id = req.get('audio_stream_id', None)
if audio_stream_id is None:
audio_stream_id = str(uuid.uuid4())
@@ -412,7 +608,6 @@ def dialog_tts_post(tenant_id, chat_id):
audio_stream = None
else:
audio_stream = io.BytesIO()
tts_stream_format = req.get('tts_stream_format', "mp3")
tts_sample_rate = req.get('tts_sample_rate', 8000)
logging.info(f"tts post {tts_sample_rate} {tts_stream_format}")
@@ -429,9 +624,7 @@ def dialog_tts_post(tenant_id, chat_id):
'tts_sample_rate':tts_sample_rate,
'tts_stream_format':tts_stream_format
}
with cache_lock:
audio_text_cache[audio_stream_id] = tts_info
total_chunk_size = 0
if delay_gen_audio is False:
try:
audio_stream.seek(0, io.SEEK_END)
@@ -439,14 +632,19 @@ def dialog_tts_post(tenant_id, chat_id):
audio_stream.write(b'\x00' * 100)
else:
# 确保在流的末尾写入
for chunk in tts_mdl.tts(text,sample_rate=tts_sample_rate,stream_formate=tts_stream_format):
for chunk in tts_mdl.tts(text,sample_rate=tts_sample_rate,stream_format=tts_stream_format):
total_chunk_size = total_chunk_size + len(chunk)
audio_stream.write(chunk)
audio_stream.seek(0)
except Exception as e:
logging.info(f"--error:{e}")
with cache_lock:
audio_text_cache.pop(audio_stream_id, None)
return get_error_data_result(message="get tts audio stream error.")
with cache_lock:
tts_info['chunk_size'] = total_chunk_size
audio_text_cache[audio_stream_id] = tts_info
# 构建音频流URL
audio_stream_url = f"/chats/{chat_id}/tts/{audio_stream_id}"
logging.info(f"--return request tts audio url {audio_stream_id} {audio_stream_url} "
@@ -460,17 +658,25 @@ def dialog_tts_post(tenant_id, chat_id):
return get_error_data_result(message="服务器内部错误")
def get_antique_categories(mesum_id):
res = MesumAntiqueService.get_all_categories()
res = MesumAntiqueService.get_all_categories(mesum_id)
return res
def get_labels_ext(mesum_id):
res = MesumAntiqueService.get_labels_ext(mesum_id)
return res
def get_labels_with_id(mesum_id):
res = MesumAntiqueService.get_labels_with_id(mesum_id)
return res
def get_antique_labels(mesum_id):
res = MesumAntiqueService.get_all_labels()
return res
def get_all_antiques(mesum_id):
res =[]
antiques=MesumAntiqueService.get_by_mesum_id(mesum_id)
@@ -509,10 +715,89 @@ def mesum_antique_get_brief(tenant_id,mesum_id):
def mesum_antique_get_full(tenant_id,mesum_id,antique_id):
try:
logging.info(f"mesum_antique_get_full {mesum_id} {antique_id}")
return get_result(data=MesumAntiqueService.get_antique_by_id(mesum_id,antique_id))
antique_detail =MesumAntiqueService.get_antique_by_id(mesum_id,antique_id)
# 这里是得到事先生成的tts文件地址需要根据需要返回正确可能需要根据前端的要求返回不同的地址
if antique_detail.get('ttsUrl_adult'):
antique_detail['ttsUrl'] = antique_detail.get('ttsUrl_adult')
return get_result(data=antique_detail)
except Exception as e:
return get_error_data_result(message=f"Get mesum antique error {e}")
@manager.route('/mesum/antique/<action>/<mesum_id>/<antique_id>', methods=['POST'])
@token_required
def mesum_antique_action(tenant_id,action,mesum_id,antique_id):
req = request.json
try:
logging.info(f"mesum_antique_action {action} {mesum_id} {antique_id}")
if action.lower() not in ['rm','update','insert']:
return get_result(data={"error": f"{action} not supported"})
if action.lower() == "rm": # 删除
res=MesumAntiqueService.delete_by_id(antique_id)
return get_result(data={"rm":res})
if action.lower() == "update":
record = req
logging.info(f"mesum_antique_action {action} {record}")
res = MesumAntiqueService.update_by_id(antique_id,record)
logging.info(f"mesum_antique_action return {action} {record}")
return get_result(data={"update": res})
if action.lower() == "insert":
record = req
logging.info(f"mesum_antique_action {action} {record}")
res = MesumAntiqueService.insert(**record)
return get_result(data={"insert": res})
except Exception as e:
return get_error_data_result(message=f"antique {action} error {e}")
# 20250428 增加操作minio 的调用API
minio_client = RAGFlowMinio()
@manager.route('/minio/check', methods=['POST'])
@token_required
def minio_check_obj(tenant_id):
req = request.json
try:
is_exist = minio_client.obj_exist(req.get('bucket'),req.get('file_name'))
return get_result(data={"is_exits":is_exist})
except Exception as e:
return get_error_data_result(message=f"minio check object error {e}")
@manager.route('/minio/get/<bucket>/<file_name>', methods=['GET'])
@token_required
def minio_get_obj(tenant_id,bucket,file_name):
try:
res= minio_client.get(bucket,file_name)
return get_result(data={"binary":res})
except Exception as e:
return get_error_data_result(message=f"minio get object error {e}")
@manager.route('/minio/rm', methods=['POST'])
@token_required
def minio_rm_obj(tenant_id):
req = request.json
try:
minio_client.rm(req.get('bucket'), req.get('file_name'))
return get_result(data={"rm": f"{req.get('bucket')}{req.get('file_name')}"})
except Exception as e:
return get_error_data_result(message=f"minio rm object error {e}")
@manager.route('/minio/put', methods=['POST'])
@token_required
def minio_put_obj(tenant_id):
req = request.form
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
try:
file = request.files['file']
bucket = req.get('bucket')
file_name = req.get('file_name')
binary = file.read()
res=minio_client.put(bucket,file_name ,binary)
return get_result(data={"put": f"{res}",'url':f"http://1.13.185.116:9000/{bucket}/{file_name}"})
except Exception as e:
return get_error_data_result(message=f"minio put object error {e}")
#------------------------------------------------
def audio_fade_in(audio_data, fade_length):
# 假设音频数据是16位单声道PCM
# 将二进制数据转换为整数数组
@@ -524,4 +809,19 @@ def audio_fade_in(audio_data, fade_length):
samples[i] = int(samples[i] * fade_factor)
# 将整数数组转换回二进制数据
return samples.tobytes()
return samples.tobytes( )
def parse_markdown_json(json_string):
# 使用正则表达式匹配Markdown中的JSON代码块
match = re.search(r'```json\n(.*?)\n```', json_string, re.DOTALL)
if match:
try:
# 尝试解析JSON字符串
data = json.loads(match[1])
return {'success': True, 'data': data }
except json.JSONDecodeError as e:
# 如果解析失败,返回错误信息
return {'success': False, 'data': str(e)}
else:
return {'success': False, 'data': 'not a valid markdown json string'}