security: 安全规范 + 敏感数据脱敏
- CLAUDE.md 新增安全规范章节(禁止提交 / 必须提供 .env.example) - finance/config/settings.py 硬编码密码移除 - djapi/api/video/audioRead.py API Key 替换为占位符 - 新增 finance/.env.example 示例配置 - .gitignore 解除 .env.example 排除 Co-Authored-By: Simon <simon@doorcome.cn>
This commit is contained in:
@@ -30,6 +30,9 @@ Thumbs.db
|
|||||||
*.token
|
*.token
|
||||||
credentials.*
|
credentials.*
|
||||||
|
|
||||||
|
# Env examples are safe to commit
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@@ -78,3 +78,28 @@ python cli/demo_sentiment_detail.py --ts_code 600519.SH --date 20260603
|
|||||||
| NLP | Qwen (DashScope / Ollama) | .env 配置 |
|
| NLP | Qwen (DashScope / Ollama) | .env 配置 |
|
||||||
| Agent | 自研编排器 | finance/agents/ |
|
| Agent | 自研编排器 | finance/agents/ |
|
||||||
| API | Django 5.2 + uWSGI | djapi/ |
|
| API | Django 5.2 + uWSGI | djapi/ |
|
||||||
|
|
||||||
|
## 安全规范
|
||||||
|
|
||||||
|
### 禁止提交
|
||||||
|
|
||||||
|
- `.env`(含真实 key)
|
||||||
|
- API Key(`sk-*`、`TUSHARE_TOKEN` 等)
|
||||||
|
- Cookie / Session
|
||||||
|
- Token / 密钥
|
||||||
|
- 个人隐私数据(手机号、身份证、密码)
|
||||||
|
|
||||||
|
### 必须提供
|
||||||
|
|
||||||
|
- `.env.example` — 仅含占位符的示例配置,如:
|
||||||
|
```
|
||||||
|
TUSHARE_TOKEN=your_token_here
|
||||||
|
QWEN_API_KEY=sk-your-key-here
|
||||||
|
MAC_DB_PASSWORD=your_password_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提交前检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -r "sk-\|token\|_H(lU\|password" --include="*.py" --include="*.md" --include="*.yaml" | grep -v ".example\|your_token\|your_password"
|
||||||
|
```
|
||||||
|
|||||||
+438
-438
@@ -1,439 +1,439 @@
|
|||||||
import env # 加载 .env 到环境变量
|
import env # 加载 .env 到环境变量
|
||||||
import os
|
import os
|
||||||
import dashscope
|
import dashscope
|
||||||
import pydub
|
import pydub
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from pydub.silence import split_on_silence
|
from pydub.silence import split_on_silence
|
||||||
from dashscope.audio.asr import Recognition
|
from dashscope.audio.asr import Recognition
|
||||||
from dashscope import Generation
|
from dashscope import Generation
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from mysqlHandle import MySQLDB
|
from mysqlHandle import MySQLDB
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
# 设置环境变量
|
# 设置环境变量
|
||||||
#os.environ["DASHSCOPE_API_KEY"] = "sk-d2d65b726068445b98b88fc3b675dbf1" # 替换为你的API Key
|
# os.environ["DASHSCOPE_API_KEY"] = "sk-your-dashscope-key"
|
||||||
|
|
||||||
def convert_mp3_to_wav(mp3_path, output_wav_path):
|
def convert_mp3_to_wav(mp3_path, output_wav_path):
|
||||||
"""
|
"""
|
||||||
将MP3文件转换为16kHz单声道WAV格式,这是Qwen3-ASR-Flash模型的推荐格式
|
将MP3文件转换为16kHz单声道WAV格式,这是Qwen3-ASR-Flash模型的推荐格式
|
||||||
参数:
|
参数:
|
||||||
mp3_path (str): MP3文件路径
|
mp3_path (str): MP3文件路径
|
||||||
output_wav_path (str): 输出WAV文件路径
|
output_wav_path (str): 输出WAV文件路径
|
||||||
返回值:
|
返回值:
|
||||||
str: 转换后的WAV文件路径
|
str: 转换后的WAV文件路径
|
||||||
"""
|
"""
|
||||||
logger.info(f"开始转换MP3到WAV: {mp3_path}")
|
logger.info(f"开始转换MP3到WAV: {mp3_path}")
|
||||||
# 加载MP3文件
|
# 加载MP3文件
|
||||||
audio = AudioSegment.from_file(mp3_path, format="mp3")
|
audio = AudioSegment.from_file(mp3_path, format="mp3")
|
||||||
# 转换为16kHz采样率、单声道、16位深度
|
# 转换为16kHz采样率、单声道、16位深度
|
||||||
audio = audio.set_frame_rate(16000).set_channels(1)
|
audio = audio.set_frame_rate(16000).set_channels(1)
|
||||||
# 导出为WAV格式
|
# 导出为WAV格式
|
||||||
audio.export(output_wav_path, format="wav")
|
audio.export(output_wav_path, format="wav")
|
||||||
logger.info(f"✓ MP3转换完成: {output_wav_path}")
|
logger.info(f"✓ MP3转换完成: {output_wav_path}")
|
||||||
#print(f"✓ MP3转换完成: {output_wav_path}")
|
#print(f"✓ MP3转换完成: {output_wav_path}")
|
||||||
return output_wav_path
|
return output_wav_path
|
||||||
|
|
||||||
def split_audio_by_fixed_duration(audio_path, chunk_duration, output_folder):
|
def split_audio_by_fixed_duration(audio_path, chunk_duration, output_folder):
|
||||||
"""
|
"""
|
||||||
将音频文件按固定时长分割成多个片段
|
将音频文件按固定时长分割成多个片段
|
||||||
参数:
|
参数:
|
||||||
audio_path (str): 音频文件路径
|
audio_path (str): 音频文件路径
|
||||||
chunk_duration (int): 分片时长(毫秒)
|
chunk_duration (int): 分片时长(毫秒)
|
||||||
output_folder (str): 输出文件夹路径
|
output_folder (str): 输出文件夹路径
|
||||||
返回值:
|
返回值:
|
||||||
list: 分片文件路径列表
|
list: 分片文件路径列表
|
||||||
"""
|
"""
|
||||||
# 加载音频文件
|
# 加载音频文件
|
||||||
audio = AudioSegment.from_file(audio_path)
|
audio = AudioSegment.from_file(audio_path)
|
||||||
# 计算总时长(毫秒)
|
# 计算总时长(毫秒)
|
||||||
total_duration = len(audio)
|
total_duration = len(audio)
|
||||||
# 分片数
|
# 分片数
|
||||||
num_chunks = total_duration // chunk_duration + 1
|
num_chunks = total_duration // chunk_duration + 1
|
||||||
# 存储分片文件路径
|
# 存储分片文件路径
|
||||||
chunks = []
|
chunks = []
|
||||||
|
|
||||||
# 创建输出文件夹
|
# 创建输出文件夹
|
||||||
os.makedirs(output_folder, exist_ok=True)
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
|
|
||||||
logger.info(f"开始音频分割,总时长: {total_duration/1000:.1f}秒,将分割为{num_chunks}个片段")
|
logger.info(f"开始音频分割,总时长: {total_duration/1000:.1f}秒,将分割为{num_chunks}个片段")
|
||||||
|
|
||||||
for i in range(num_chunks):
|
for i in range(num_chunks):
|
||||||
# 计算当前分片的起始和结束时间
|
# 计算当前分片的起始和结束时间
|
||||||
start_time = i * chunk_duration
|
start_time = i * chunk_duration
|
||||||
end_time = (i + 1) * chunk_duration
|
end_time = (i + 1) * chunk_duration
|
||||||
# 提取分片音频
|
# 提取分片音频
|
||||||
chunk = audio[start_time:end_time]
|
chunk = audio[start_time:end_time]
|
||||||
# 生成文件名
|
# 生成文件名
|
||||||
chunk_name = f"chunk_{i}.wav"
|
chunk_name = f"chunk_{i}.wav"
|
||||||
chunk_path = os.path.join(output_folder, chunk_name)
|
chunk_path = os.path.join(output_folder, chunk_name)
|
||||||
# 导出分片音频
|
# 导出分片音频
|
||||||
chunk.export(chunk_path, format="wav")
|
chunk.export(chunk_path, format="wav")
|
||||||
chunks.append(chunk_path)
|
chunks.append(chunk_path)
|
||||||
|
|
||||||
# 打印处理进度
|
# 打印处理进度
|
||||||
progress = (i + 1) / num_chunks * 100
|
progress = (i + 1) / num_chunks * 100
|
||||||
logger.info(f"✓ 已完成分片 {i+1}/{num_chunks} ({progress:.1f}%)")
|
logger.info(f"✓ 已完成分片 {i+1}/{num_chunks} ({progress:.1f}%)")
|
||||||
|
|
||||||
logger.info(f"✓ 音频分割完成,共生成{len(chunks)}个分片文件")
|
logger.info(f"✓ 音频分割完成,共生成{len(chunks)}个分片文件")
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
def split_audio_by_smart_silence(audio_path, min_silence_len, silence_thresh, output_folder):
|
def split_audio_by_smart_silence(audio_path, min_silence_len, silence_thresh, output_folder):
|
||||||
"""
|
"""
|
||||||
将音频文件按智能静音检测方式分割成多个片段,每段不超过3分钟
|
将音频文件按智能静音检测方式分割成多个片段,每段不超过3分钟
|
||||||
参数:
|
参数:
|
||||||
audio_path (str): 音频文件路径
|
audio_path (str): 音频文件路径
|
||||||
min_silence_len (int): 最小静音长度(毫秒)
|
min_silence_len (int): 最小静音长度(毫秒)
|
||||||
silence_thresh (int): 静音阈值(dBFS)
|
silence_thresh (int): 静音阈值(dBFS)
|
||||||
output_folder (str): 输出文件夹路径
|
output_folder (str): 输出文件夹路径
|
||||||
返回值:
|
返回值:
|
||||||
list: 分片文件路径列表
|
list: 分片文件路径列表
|
||||||
"""
|
"""
|
||||||
# 加载音频文件
|
# 加载音频文件
|
||||||
audio = AudioSegment.from_file(audio_path, format="wav")
|
audio = AudioSegment.from_file(audio_path, format="wav")
|
||||||
# 按静音分割
|
# 按静音分割
|
||||||
segments = split_on_silence(
|
segments = split_on_silence(
|
||||||
audio,
|
audio,
|
||||||
# 静音超过700毫秒则分割
|
# 静音超过700毫秒则分割
|
||||||
min_silence_len=min_silence_len,
|
min_silence_len=min_silence_len,
|
||||||
# 静音阈值为-40dBFS
|
# 静音阈值为-40dBFS
|
||||||
silence_thresh=silence_thresh,
|
silence_thresh=silence_thresh,
|
||||||
# 保留静音部分
|
# 保留静音部分
|
||||||
keep_silence=400
|
keep_silence=400
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"✓ 静音分割完成,共{len(segments)}个初始片段")
|
logger.info(f"✓ 静音分割完成,共{len(segments)}个初始片段")
|
||||||
|
|
||||||
# 合并过短的片段
|
# 合并过短的片段
|
||||||
merged_segments = []
|
merged_segments = []
|
||||||
current_segment = None
|
current_segment = None
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
if current_segment is None:
|
if current_segment is None:
|
||||||
current_segment = segment
|
current_segment = segment
|
||||||
else:
|
else:
|
||||||
# 合并当前片段和新片段
|
# 合并当前片段和新片段
|
||||||
temp_segment = current_segment + segment
|
temp_segment = current_segment + segment
|
||||||
# 如果合并后的片段超过3分钟,则单独保存当前片段
|
# 如果合并后的片段超过3分钟,则单独保存当前片段
|
||||||
if len(temp_segment) > 180000: # 3分钟=180,000毫秒
|
if len(temp_segment) > 180000: # 3分钟=180,000毫秒
|
||||||
merged_segments.append(current_segment)
|
merged_segments.append(current_segment)
|
||||||
current_segment = segment
|
current_segment = segment
|
||||||
else:
|
else:
|
||||||
current_segment = temp_segment
|
current_segment = temp_segment
|
||||||
# 添加最后一个片段
|
# 添加最后一个片段
|
||||||
if current_segment is not None:
|
if current_segment is not None:
|
||||||
merged_segments.append(current_segment)
|
merged_segments.append(current_segment)
|
||||||
|
|
||||||
logger.info(f"✓ 片段合并完成,共{len(merged_segments)}个最终片段")
|
logger.info(f"✓ 片段合并完成,共{len(merged_segments)}个最终片段")
|
||||||
|
|
||||||
# 存储分片文件路径
|
# 存储分片文件路径
|
||||||
chunks = []
|
chunks = []
|
||||||
|
|
||||||
# 创建输出文件夹
|
# 创建输出文件夹
|
||||||
os.makedirs(output_folder, exist_ok=True)
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
|
|
||||||
logger.info(f"开始导出音频片段到: {output_folder}")
|
logger.info(f"开始导出音频片段到: {output_folder}")
|
||||||
|
|
||||||
for i, segment in enumerate(merged_segments):
|
for i, segment in enumerate(merged_segments):
|
||||||
# 生成文件名
|
# 生成文件名
|
||||||
chunk_name = f"chunk_{i}.wav"
|
chunk_name = f"chunk_{i}.wav"
|
||||||
chunk_path = os.path.join(output_folder, chunk_name)
|
chunk_path = os.path.join(output_folder, chunk_name)
|
||||||
# 导出分片音频
|
# 导出分片音频
|
||||||
segment.export(chunk_path, format="wav")
|
segment.export(chunk_path, format="wav")
|
||||||
chunks.append(chunk_path)
|
chunks.append(chunk_path)
|
||||||
|
|
||||||
# 打印处理进度
|
# 打印处理进度
|
||||||
progress = (i + 1) / len(merged_segments) * 100
|
progress = (i + 1) / len(merged_segments) * 100
|
||||||
logger.info(f"✓ 已完成分片 {i+1}/{len(merged_segments)} ({progress:.1f}%)")
|
logger.info(f"✓ 已完成分片 {i+1}/{len(merged_segments)} ({progress:.1f}%)")
|
||||||
|
|
||||||
logger.info(f"✓ 智能静音分割完成,共生成{len(chunks)}个分片文件")
|
logger.info(f"✓ 智能静音分割完成,共生成{len(chunks)}个分片文件")
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
def transcribe_audio(audio_path):
|
def transcribe_audio(audio_path):
|
||||||
"""
|
"""
|
||||||
使用Paraformer实时语音识别模型(通过本地文件)转录音频文件
|
使用Paraformer实时语音识别模型(通过本地文件)转录音频文件
|
||||||
参数:
|
参数:
|
||||||
audio_path (str): 音频文件路径(必须是16kHz单声道WAV)
|
audio_path (str): 音频文件路径(必须是16kHz单声道WAV)
|
||||||
返回值:
|
返回值:
|
||||||
str: 识别文本,如果失败返回空字符串
|
str: 识别文本,如果失败返回空字符串
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 确保音频文件存在
|
# 确保音频文件存在
|
||||||
if not os.path.exists(audio_path):
|
if not os.path.exists(audio_path):
|
||||||
logger.error(f"音频文件不存在: {audio_path}")
|
logger.error(f"音频文件不存在: {audio_path}")
|
||||||
return ['', '']
|
return ['', '']
|
||||||
dashscope.api_key = os.getenv('DASHSCOPE_API_KEY', '')
|
dashscope.api_key = os.getenv('DASHSCOPE_API_KEY', '')
|
||||||
# 创建识别对象
|
# 创建识别对象
|
||||||
recognition = Recognition(
|
recognition = Recognition(
|
||||||
model=os.getenv('DASHSCOPE_ASR_MODEL', 'paraformer-realtime-v2'),
|
model=os.getenv('DASHSCOPE_ASR_MODEL', 'paraformer-realtime-v2'),
|
||||||
format='wav',
|
format='wav',
|
||||||
sample_rate=16000,
|
sample_rate=16000,
|
||||||
language_hints=['zh','en'], # 中文和英文
|
language_hints=['zh','en'], # 中文和英文
|
||||||
callback=None
|
callback=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# 调用识别
|
# 调用识别
|
||||||
logger.info(f"开始识别音频: {audio_path}")
|
logger.info(f"开始识别音频: {audio_path}")
|
||||||
result = recognition.call(audio_path)
|
result = recognition.call(audio_path)
|
||||||
text=[]
|
text=[]
|
||||||
if result.status_code == HTTPStatus.OK:
|
if result.status_code == HTTPStatus.OK:
|
||||||
# 提取识别结果
|
# 提取识别结果
|
||||||
logger.info(f"✓ {audio_path} 识别成功")
|
logger.info(f"✓ {audio_path} 识别成功")
|
||||||
sentence = result.get_sentence()
|
sentence = result.get_sentence()
|
||||||
text.append(merge_transcripts(sentence))
|
text.append(merge_transcripts(sentence))
|
||||||
logger.info(f"识别文本长度: {len(text[0])}")
|
logger.info(f"识别文本长度: {len(text[0])}")
|
||||||
text.append(analyze_and_correct_text(text[0]))
|
text.append(analyze_and_correct_text(text[0]))
|
||||||
return text
|
return text
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ 任务失败: {result.message}")
|
logger.error(f"❌ 任务失败: {result.message}")
|
||||||
return ['', '']
|
return ['', '']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"识别过程中发生异常: {e}")
|
logger.error(f"识别过程中发生异常: {e}")
|
||||||
return ['', '']
|
return ['', '']
|
||||||
|
|
||||||
def merge_transcripts(transcripts):
|
def merge_transcripts(transcripts):
|
||||||
"""
|
"""
|
||||||
将多段识别文本合并成完整句子(保留原始段落逻辑,用空格连接)
|
将多段识别文本合并成完整句子(保留原始段落逻辑,用空格连接)
|
||||||
参数:
|
参数:
|
||||||
transcripts (list): 识别结果列表,每个元素为字典{'text': '识别文本'}
|
transcripts (list): 识别结果列表,每个元素为字典{'text': '识别文本'}
|
||||||
返回:
|
返回:
|
||||||
str: 合并后的完整文本
|
str: 合并后的完整文本
|
||||||
"""
|
"""
|
||||||
# 输入参数检查
|
# 输入参数检查
|
||||||
if not transcripts:
|
if not transcripts:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# 确保transcripts是可迭代对象
|
# 确保transcripts是可迭代对象
|
||||||
if not hasattr(transcripts, '__iter__'):
|
if not hasattr(transcripts, '__iter__'):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 提取所有有效的text字段
|
# 提取所有有效的text字段
|
||||||
texts = []
|
texts = []
|
||||||
for t in transcripts:
|
for t in transcripts:
|
||||||
try:
|
try:
|
||||||
# 检查是否为字典类型且包含text字段
|
# 检查是否为字典类型且包含text字段
|
||||||
if isinstance(t, dict) and 'text' in t and t['text']:
|
if isinstance(t, dict) and 'text' in t and t['text']:
|
||||||
text = t['text']
|
text = t['text']
|
||||||
# 确保text是字符串类型
|
# 确保text是字符串类型
|
||||||
if isinstance(text, str) and text.strip():
|
if isinstance(text, str) and text.strip():
|
||||||
texts.append(text.strip())
|
texts.append(text.strip())
|
||||||
except (KeyError, TypeError, AttributeError):
|
except (KeyError, TypeError, AttributeError):
|
||||||
# 忽略单个元素的处理错误,继续处理其他元素
|
# 忽略单个元素的处理错误,继续处理其他元素
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 用空格连接所有段落(根据实际需求可调整连接符)
|
# 用空格连接所有段落(根据实际需求可调整连接符)
|
||||||
return " ".join(texts) if texts else ""
|
return " ".join(texts) if texts else ""
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"合并转录文本时发生错误: {e}")
|
logger.error(f"合并转录文本时发生错误: {e}")
|
||||||
return ""
|
return ""
|
||||||
def text_correction(text):
|
def text_correction(text):
|
||||||
"""
|
"""
|
||||||
使用通义千问模型修正文本中的错误和标点符号
|
使用通义千问模型修正文本中的错误和标点符号
|
||||||
参数:
|
参数:
|
||||||
text (str): 需要修正的文本
|
text (str): 需要修正的文本
|
||||||
返回值:
|
返回值:
|
||||||
str: 修正后的文本
|
str: 修正后的文本
|
||||||
"""
|
"""
|
||||||
logger.info("开始文本修正...")
|
logger.info("开始文本修正...")
|
||||||
|
|
||||||
# 构建修正提示词
|
# 构建修正提示词
|
||||||
correction_prompt = """请仔细检查以下文本,修正其中的错误:
|
correction_prompt = """请仔细检查以下文本,修正其中的错误:
|
||||||
1. 错别字和语法错误
|
1. 错别字和语法错误
|
||||||
2. 标点符号使用错误
|
2. 标点符号使用错误
|
||||||
3. 语句不通顺的地方
|
3. 语句不通顺的地方
|
||||||
4. 逻辑不清晰的部分
|
4. 逻辑不清晰的部分
|
||||||
|
|
||||||
请直接返回修正后的完整文本,不要添加任何解释说明。"""
|
请直接返回修正后的完整文本,不要添加任何解释说明。"""
|
||||||
|
|
||||||
# 构建消息列表
|
# 构建消息列表
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": "你是一个专业的文本校对助手,擅长修正文本中的各种错误。"},
|
{"role": "system", "content": "你是一个专业的文本校对助手,擅长修正文本中的各种错误。"},
|
||||||
{"role": "user", "content": correction_prompt},
|
{"role": "user", "content": correction_prompt},
|
||||||
{"role": "user", "content": text}
|
{"role": "user", "content": text}
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info("调用通义千问模型进行文本修正...")
|
logger.info("调用通义千问模型进行文本修正...")
|
||||||
# 调用DashScope文本生成接口
|
# 调用DashScope文本生成接口
|
||||||
response = Generation.call(
|
response = Generation.call(
|
||||||
model=os.getenv('DASHSCOPE_LLM_MODEL', 'qwen-plus'),
|
model=os.getenv('DASHSCOPE_LLM_MODEL', 'qwen-plus'),
|
||||||
messages=messages,
|
messages=messages,
|
||||||
max_tokens=30000,
|
max_tokens=30000,
|
||||||
temperature=0.1, # 使用较低的温度以提高确定性
|
temperature=0.1, # 使用较低的温度以提高确定性
|
||||||
top_p=0.5
|
top_p=0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查API调用是否成功
|
# 检查API调用是否成功
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.warning(f"❌ 文本修正API调用失败: {response.message}")
|
logger.warning(f"❌ 文本修正API调用失败: {response.message}")
|
||||||
raise Exception(f"文本修正API调用失败: {response.message}")
|
raise Exception(f"文本修正API调用失败: {response.message}")
|
||||||
|
|
||||||
logger.info("✓ 文本修正完成")
|
logger.info("✓ 文本修正完成")
|
||||||
# 返回修正后的文本
|
# 返回修正后的文本
|
||||||
return response.output.text
|
return response.output.text
|
||||||
|
|
||||||
def analyze_and_correct_text(text):
|
def analyze_and_correct_text(text):
|
||||||
"""
|
"""
|
||||||
分析文本并自动修正错误
|
分析文本并自动修正错误
|
||||||
参数:
|
参数:
|
||||||
text (str): 待分析和修正的文本
|
text (str): 待分析和修正的文本
|
||||||
prompt (str): 分析提示词
|
prompt (str): 分析提示词
|
||||||
返回值:
|
返回值:
|
||||||
tuple: (修正后的文本, 分析结果)
|
tuple: (修正后的文本, 分析结果)
|
||||||
"""
|
"""
|
||||||
logger.info("开始文本分析和修正流程...")
|
logger.info("开始文本分析和修正流程...")
|
||||||
|
|
||||||
# 首先修正文本错误
|
# 首先修正文本错误
|
||||||
corrected_text = text_correction(text)
|
corrected_text = text_correction(text)
|
||||||
if corrected_text is None:
|
if corrected_text is None:
|
||||||
logger.warning("文本修正返回 None,使用原始文本")
|
logger.warning("文本修正返回 None,使用原始文本")
|
||||||
corrected_text = text
|
corrected_text = text
|
||||||
logger.info(f"原始文本长度: {len(text)}")
|
logger.info(f"原始文本长度: {len(text)}")
|
||||||
logger.info(f"修正后文本长度: {len(corrected_text)}")
|
logger.info(f"修正后文本长度: {len(corrected_text)}")
|
||||||
|
|
||||||
# 使用修正后的文本进行分析
|
# 使用修正后的文本进行分析
|
||||||
# analysis_result = analyze_text(corrected_text, prompt)
|
# analysis_result = analyze_text(corrected_text, prompt)
|
||||||
|
|
||||||
return corrected_text
|
return corrected_text
|
||||||
def analyze_text(text, prompt):
|
def analyze_text(text, prompt):
|
||||||
"""
|
"""
|
||||||
使用通义千问模型分析文本
|
使用通义千问模型分析文本
|
||||||
参数:
|
参数:
|
||||||
text (str): 待分析文本
|
text (str): 待分析文本
|
||||||
prompt (str): 分析提示词
|
prompt (str): 分析提示词
|
||||||
返回值:
|
返回值:
|
||||||
str: 分析结果
|
str: 分析结果
|
||||||
"""
|
"""
|
||||||
logger.info("开始文本分析...")
|
logger.info("开始文本分析...")
|
||||||
# 设置系统提示
|
# 设置系统提示
|
||||||
system_prompt = "你是一个专业的文本分析助手,擅长根据提示词对长文本进行深入分析。"
|
system_prompt = "你是一个专业的文本分析助手,擅长根据提示词对长文本进行深入分析。"
|
||||||
# 构建消息列表
|
# 构建消息列表
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": system_prompt},
|
{"role": "system", "content": system_prompt},
|
||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
{"role": "user", "content": text}
|
{"role": "user", "content": text}
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info("调用通义千问模型进行文本分析...")
|
logger.info("调用通义千问模型进行文本分析...")
|
||||||
# 调用DashScope文本生成接口
|
# 调用DashScope文本生成接口
|
||||||
response = Generation.call(
|
response = Generation.call(
|
||||||
model=os.getenv('DASHSCOPE_LLM_MODEL', 'qwen-plus'),
|
model=os.getenv('DASHSCOPE_LLM_MODEL', 'qwen-plus'),
|
||||||
messages=messages,
|
messages=messages,
|
||||||
max_tokens=8190, # 控制生成文本的最大长度
|
max_tokens=8190, # 控制生成文本的最大长度
|
||||||
temperature=0.3, # 控制生成文本的确定性
|
temperature=0.3, # 控制生成文本的确定性
|
||||||
top_p=0.7 # 控制生成文本的多样性
|
top_p=0.7 # 控制生成文本的多样性
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查API调用是否成功
|
# 检查API调用是否成功
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"❌ API调用失败: {response.message}")
|
logger.error(f"❌ API调用失败: {response.message}")
|
||||||
raise Exception(f"API调用失败: {response.message}")
|
raise Exception(f"API调用失败: {response.message}")
|
||||||
|
|
||||||
logger.info("✓ 文本分析完成")
|
logger.info("✓ 文本分析完成")
|
||||||
# 返回分析结果
|
# 返回分析结果
|
||||||
return response.output.text
|
return response.output.text
|
||||||
|
|
||||||
def process_long_audio(mp3_path, output_folder, date_str):
|
def process_long_audio(mp3_path, output_folder, date_str):
|
||||||
"""
|
"""
|
||||||
处理长音频文件,分割、识别并分析
|
处理长音频文件,分割、识别并分析
|
||||||
参数:
|
参数:
|
||||||
mp3_path (str): MP3文件路径
|
mp3_path (str): MP3文件路径
|
||||||
prompt (str): 分析提示词
|
prompt (str): 分析提示词
|
||||||
output_folder (str): 输出文件夹路径
|
output_folder (str): 输出文件夹路径
|
||||||
返回值:
|
返回值:
|
||||||
str: 分析结果
|
str: 分析结果
|
||||||
"""
|
"""
|
||||||
logger.info("开始处理长音频...")
|
logger.info("开始处理长音频...")
|
||||||
|
|
||||||
# 转换MP3为WAV格式
|
# 转换MP3为WAV格式
|
||||||
logger.info("步骤1/4: 转换MP3为WAV格式")
|
logger.info("步骤1/4: 转换MP3为WAV格式")
|
||||||
wav_path = convert_mp3_to_wav(
|
wav_path = convert_mp3_to_wav(
|
||||||
mp3_path, os.path.join(output_folder, "input.wav")
|
mp3_path, os.path.join(output_folder, "input.wav")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 分割音频
|
# 分割音频
|
||||||
# 可以选择固定分片或智能静音分割
|
# 可以选择固定分片或智能静音分割
|
||||||
# chunks = split_audio_by_fixed_duration(wav_path, 180000, output_folder)
|
# chunks = split_audio_by_fixed_duration(wav_path, 180000, output_folder)
|
||||||
logger.info("步骤2/4: 智能静音分割音频")
|
logger.info("步骤2/4: 智能静音分割音频")
|
||||||
chunks = split_audio_by_smart_silence(
|
chunks = split_audio_by_smart_silence(
|
||||||
wav_path, 700, -40, output_folder
|
wav_path, 700, -40, output_folder
|
||||||
)
|
)
|
||||||
|
|
||||||
# 存储所有识别文本
|
# 存储所有识别文本
|
||||||
transcribed_text = ""
|
transcribed_text = ""
|
||||||
|
|
||||||
# 识别每个分片
|
# 识别每个分片
|
||||||
logger.info(f"步骤3/4: 开始识别音频分片,共{len(chunks)}个分片")
|
logger.info(f"步骤3/4: 开始识别音频分片,共{len(chunks)}个分片")
|
||||||
for i, chunk_path in enumerate(chunks):
|
for i, chunk_path in enumerate(chunks):
|
||||||
try:
|
try:
|
||||||
logger.info(f"识别进度: {i+1}/{len(chunks)} ({((i+1)/len(chunks)*100):.1f}%)")
|
logger.info(f"识别进度: {i+1}/{len(chunks)} ({((i+1)/len(chunks)*100):.1f}%)")
|
||||||
# 调用音频识别API
|
# 调用音频识别API
|
||||||
text = transcribe_audio(chunk_path)
|
text = transcribe_audio(chunk_path)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not text[1].startswith('今天的新闻联播节目播送完毕'):
|
if not text[1].startswith('今天的新闻联播节目播送完毕'):
|
||||||
prompt='请分析所给文本的新闻内容,返回一个简短标题'
|
prompt='请分析所给文本的新闻内容,返回一个简短标题'
|
||||||
text.append(analyze_text(text[1],prompt))
|
text.append(analyze_text(text[1],prompt))
|
||||||
else:
|
else:
|
||||||
text.append('')
|
text.append('')
|
||||||
"""
|
"""
|
||||||
# 新闻标题留空
|
# 新闻标题留空
|
||||||
text.append('')
|
text.append('')
|
||||||
# 添加到总文本
|
# 添加到总文本
|
||||||
#transcribed_text += text + "\n"
|
#transcribed_text += text + "\n"
|
||||||
# 删除临时文件
|
# 删除临时文件
|
||||||
os.remove(chunk_path)
|
os.remove(chunk_path)
|
||||||
# 初始化数据库连接
|
# 初始化数据库连接
|
||||||
db = MySQLDB() # 使用默认参数连接数据库
|
db = MySQLDB() # 使用默认参数连接数据库
|
||||||
try:
|
try:
|
||||||
# 插入数据示例
|
# 插入数据示例
|
||||||
user_data = {
|
user_data = {
|
||||||
"news_days": date_str,
|
"news_days": date_str,
|
||||||
"daily_sub_id": i,
|
"daily_sub_id": i,
|
||||||
"news_raw": text[0],
|
"news_raw": text[0],
|
||||||
"news_improve": text[1],
|
"news_improve": text[1],
|
||||||
"news_title": text[2]
|
"news_title": text[2]
|
||||||
}
|
}
|
||||||
user_id = db.insert_data("xwlb_daily", user_data)
|
user_id = db.insert_data("xwlb_daily", user_data)
|
||||||
finally:
|
finally:
|
||||||
# 关闭连接
|
# 关闭连接
|
||||||
db.close()
|
db.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"识别失败: {chunk_path}, 错误: {e}")
|
logger.error(f"识别失败: {chunk_path}, 错误: {e}")
|
||||||
# 可以在这里添加重试逻辑
|
# 可以在这里添加重试逻辑
|
||||||
|
|
||||||
# 分析识别文本
|
# 分析识别文本
|
||||||
"""
|
"""
|
||||||
print("步骤4/4: 分析识别文本")
|
print("步骤4/4: 分析识别文本")
|
||||||
print(f"识别文本长度: {len(transcribed_text)}")
|
print(f"识别文本长度: {len(transcribed_text)}")
|
||||||
print(f"识别文本内容: {transcribed_text}")
|
print(f"识别文本内容: {transcribed_text}")
|
||||||
if len(transcribed_text) < 1:
|
if len(transcribed_text) < 1:
|
||||||
print("识别文本为空,跳过分析处理")
|
print("识别文本为空,跳过分析处理")
|
||||||
return "识别文本为空,无法进行分析"
|
return "识别文本为空,无法进行分析"
|
||||||
analysis_result = analyze_text(transcribed_text, prompt)
|
analysis_result = analyze_text(transcribed_text, prompt)
|
||||||
"""
|
"""
|
||||||
logger.info("✓ 长音频处理完成")
|
logger.info("✓ 长音频处理完成")
|
||||||
# 返回分析结果
|
# 返回分析结果
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# 使用示例
|
# 使用示例
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# MP3文件路径
|
# MP3文件路径
|
||||||
mp3_path = "20251002.mp3"
|
mp3_path = "20251002.mp3"
|
||||||
# 分析提示词
|
# 分析提示词
|
||||||
prompt = "请总结这段由中国中央电视台新闻联播音频转为文字的文本,理解其主要内容并提取其中的关键信息。"
|
prompt = "请总结这段由中国中央电视台新闻联播音频转为文字的文本,理解其主要内容并提取其中的关键信息。"
|
||||||
# 输出文件夹
|
# 输出文件夹
|
||||||
output_folder = "audio_processing"
|
output_folder = "audio_processing"
|
||||||
|
|
||||||
# 处理长音频
|
# 处理长音频
|
||||||
try:
|
try:
|
||||||
result = process_long_audio(
|
result = process_long_audio(
|
||||||
mp3_path, prompt, output_folder
|
mp3_path, prompt, output_folder
|
||||||
)
|
)
|
||||||
# 打印分析结果
|
# 打印分析结果
|
||||||
print("分析结果:\n")
|
print("分析结果:\n")
|
||||||
print(result)
|
print(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"处理失败: {e}")
|
print(f"处理失败: {e}")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# ── Qwen API 配置(DashScope) ─────────────────────────────
|
||||||
|
# API Key 获取:https://dashscope.console.aliyun.com/apiKey
|
||||||
|
QWEN_API_KEY=sk-your-key-here
|
||||||
|
QWEN_MODEL=qwen-turbo
|
||||||
|
|
||||||
|
# ── 本地 Qwen 部署(可选,优先级高于 API) ──────────────────
|
||||||
|
# QWEN_LOCAL_BASE_URL=http://localhost:11434/v1
|
||||||
|
# QWEN_LOCAL_MODEL=qwen2.5:7b
|
||||||
|
|
||||||
|
# ── Tushare 数据源 ─────────────────────────────────────────
|
||||||
|
# Token 获取:https://tushare.pro 注册后在 "个人主页→接口TOKEN" 复制
|
||||||
|
TUSHARE_TOKEN=your_token_here
|
||||||
|
|
||||||
|
# ── 数据库 ──────────────────────────────────────────────────
|
||||||
|
MAC_DB_HOST=127.0.0.1
|
||||||
|
MAC_DB_PORT=13306
|
||||||
|
MAC_DB_USER=myquant
|
||||||
|
MAC_DB_PASSWORD=your_password_here
|
||||||
|
MAC_DB_NAME=myquant
|
||||||
|
|
||||||
|
# ── 情绪分析范围 ────────────────────────────────────────────
|
||||||
|
SENTIMENT_SCOPE_TYPE=index
|
||||||
|
SENTIMENT_SCOPE_INDEXES=000300,000905
|
||||||
|
SENTIMENT_MAX_NEWS_PER_STOCK=30
|
||||||
|
|
||||||
|
# ── MCP 新闻服务(可选) ────────────────────────────────────
|
||||||
|
# NEWS_MCP_URL=http://192.168.1.160:3333/mcp
|
||||||
@@ -18,7 +18,7 @@ MARIADB_CONFIG = {
|
|||||||
"host": os.getenv("MAC_DB_HOST", "127.0.0.1"),
|
"host": os.getenv("MAC_DB_HOST", "127.0.0.1"),
|
||||||
"port": int(os.getenv("MAC_DB_PORT", "13306")),
|
"port": int(os.getenv("MAC_DB_PORT", "13306")),
|
||||||
"user": os.getenv("MAC_DB_USER", "myquant"),
|
"user": os.getenv("MAC_DB_USER", "myquant"),
|
||||||
"password": os.getenv("MAC_DB_PASSWORD", "_H(lU1_fF*9baRTp"),
|
"password": os.getenv("MAC_DB_PASSWORD", ""),
|
||||||
"database": os.getenv("MAC_DB_NAME", "myquant"),
|
"database": os.getenv("MAC_DB_NAME", "myquant"),
|
||||||
"charset": "utf8mb4",
|
"charset": "utf8mb4",
|
||||||
"pool_size": 5,
|
"pool_size": 5,
|
||||||
|
|||||||
Reference in New Issue
Block a user