From 72d9ebdc07b312d95d74e73dc440784b2ba4e2bd Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Fri, 21 Nov 2025 15:47:42 +0800 Subject: [PATCH] 1.0.4 --- .DS_Store | Bin 8196 -> 8196 bytes app.zip | Bin 65959 -> 64935 bytes app/api/endpoints/meetings.py | 5 +- app/services/async_transcription_service.py | 58 +++++++---- app/services/voice_service.py | 108 -------------------- docker-compose.prod.yml | 1 + 6 files changed, 44 insertions(+), 128 deletions(-) delete mode 100644 app/services/voice_service.py diff --git a/.DS_Store b/.DS_Store index 8ef9355b9543b73cab39087dbe59ada5d0761452..408a8dd5e3f31b0516c8c8cfb9de5f7e83df1c26 100644 GIT binary patch delta 18 ZcmZp1XmQw(CcqlUz`(G4bGE=tegHR-1&;s# delta 18 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 V1;s%U01*Ir#svXM-1+?){~z!X2~7Y1 delta 13918 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 EKjvO*VgLXD diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py index 860720e..4e88c65 100644 --- a/app/api/endpoints/meetings.py +++ b/app/api/endpoints/meetings.py @@ -415,13 +415,13 @@ async def upload_audio(audio_file: UploadFile = File(...), meeting_id: int = For with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) if replaced_existing and force_replace_bool: - cursor.execute("DELETE FROM transcript_segments WHERE meeting_id = %s", (meeting_id,)) - cursor.execute("DELETE FROM transcript_tasks WHERE meeting_id = %s", (meeting_id,)) + # 只删除旧的音频文件,转录数据由 start_transcription 统一处理 if existing_info and existing_info['file_path']: old_file_path = BASE_DIR / existing_info['file_path'].lstrip('/') if old_file_path.exists(): try: os.remove(old_file_path) + print(f"Deleted old audio file: {old_file_path}") except Exception as e: print(f"Warning: Failed to delete old file {old_file_path}: {e}") if replaced_existing: @@ -431,6 +431,7 @@ async def upload_audio(audio_file: UploadFile = File(...), meeting_id: int = For connection.commit() cursor.close() try: + # start_transcription 会自动删除旧的转录数据和任务记录 task_id = transcription_service.start_transcription(meeting_id, '/'+str(relative_path)) print(f"Transcription task started with ID: {task_id}") except Exception as e: diff --git a/app/services/async_transcription_service.py b/app/services/async_transcription_service.py index 1db34f5..cd4e50d 100644 --- a/app/services/async_transcription_service.py +++ b/app/services/async_transcription_service.py @@ -24,21 +24,43 @@ class AsyncTranscriptionService: def start_transcription(self, meeting_id: int, audio_file_path: str) -> str: """ 启动异步转录任务 - + Args: meeting_id: 会议ID audio_file_path: 音频文件相对路径 - + Returns: str: 业务任务ID """ try: - # 构造完整的文件URL + # 1. 删除该会议的旧转录数据和任务记录,并清空会议总结 + print(f"Cleaning old transcription data for meeting_id: {meeting_id}") + with get_db_connection() as connection: + cursor = connection.cursor() + + # 删除旧的转录文本段落 + cursor.execute("DELETE FROM transcript_segments WHERE meeting_id = %s", (meeting_id,)) + deleted_segments = cursor.rowcount + print(f"Deleted {deleted_segments} old transcript segments") + + # 删除旧的转录任务记录 + cursor.execute("DELETE FROM transcript_tasks WHERE meeting_id = %s", (meeting_id,)) + deleted_tasks = cursor.rowcount + print(f"Deleted {deleted_tasks} old transcript tasks") + + # 清空会议总结内容 + cursor.execute("UPDATE meetings SET summary = NULL WHERE meeting_id = %s", (meeting_id,)) + print(f"Cleared summary for meeting_id: {meeting_id}") + + connection.commit() + cursor.close() + + # 2. 构造完整的文件URL file_url = f"{self.base_url}{audio_file_path}" - + print(f"Starting transcription for meeting_id: {meeting_id}, file_url: {file_url}") - - # 调用Paraformer异步API + + # 3. 调用Paraformer异步API task_response = Transcription.async_call( model='paraformer-v2', file_urls=[file_url], @@ -47,15 +69,15 @@ class AsyncTranscriptionService: diarization_enabled=True, speaker_count=10 ) - + if task_response.status_code != HTTPStatus.OK: print(f"Failed to start transcription: {task_response.status_code}, {task_response.message}") raise Exception(f"Transcription API error: {task_response.message}") - + paraformer_task_id = task_response.output.task_id business_task_id = str(uuid.uuid4()) - - # 在Redis中存储任务映射 + + # 4. 在Redis中存储任务映射 current_time = datetime.now().isoformat() task_data = { 'business_task_id': business_task_id, @@ -67,17 +89,17 @@ class AsyncTranscriptionService: 'created_at': current_time, 'updated_at': current_time } - + # 存储到Redis,过期时间24小时 self.redis_client.hset(f"task:{business_task_id}", mapping=task_data) self.redis_client.expire(f"task:{business_task_id}", 86400) - - # 在数据库中创建任务记录 + + # 5. 在数据库中创建任务记录 self._save_task_to_db(business_task_id, paraformer_task_id, meeting_id, audio_file_path) - + print(f"Transcription task created: {business_task_id}") return business_task_id - + except Exception as e: print(f"Error starting transcription: {e}") raise e @@ -391,9 +413,9 @@ class AsyncTranscriptionService: cursor = connection.cursor() # 清除该会议的现有转录分段 - delete_query = "DELETE FROM transcript_segments WHERE meeting_id = %s" - cursor.execute(delete_query, (meeting_id,)) - print(f"Deleted existing segments for meeting_id: {meeting_id}") + # delete_query = "DELETE FROM transcript_segments WHERE meeting_id = %s" + # cursor.execute(delete_query, (meeting_id,)) + # print(f"Deleted existing segments for meeting_id: {meeting_id}") # 插入新的转录分段 insert_query = ''' diff --git a/app/services/voice_service.py b/app/services/voice_service.py deleted file mode 100644 index c0ce16b..0000000 --- a/app/services/voice_service.py +++ /dev/null @@ -1,108 +0,0 @@ -from http import HTTPStatus -import requests -import json -import dashscope -from dashscope.audio.asr import Transcription -from app.core.config import QWEN_API_KEY -from app.core.database import get_db_connection - -class VoiceService: - def __init__(self): - dashscope.api_key = QWEN_API_KEY - - def transcribe(self, file_urls: list[str], meeting_id: int): - print(f"Starting transcription for meeting_id: {meeting_id}, files: {file_urls}") - - try: - task_response = Transcription.async_call( - model='paraformer-v2', - file_urls=file_urls, - language_hints=['zh', 'en'], - disfluency_removal_enabled=True, - diarization_enabled=True, - speaker_count=10 - ) - - transcribe_response = Transcription.wait(task=task_response.output.task_id) - - if transcribe_response.status_code != HTTPStatus.OK: - print(f"Transcription failed: {transcribe_response.status_code}, {transcribe_response.message}") - return - - print("Transcription task submitted successfully!") - if not (transcribe_response.output and transcribe_response.output.get('results')): - print("No transcription results found in the response.") - return - - transcription_url = transcribe_response.output['results'][0]['transcription_url'] - print(f"Fetching transcription from URL: {transcription_url}") - - response = requests.get(transcription_url) - response.raise_for_status() - transcription_data = response.json() - - self._save_segments_to_db(transcription_data, meeting_id) - - except requests.exceptions.RequestException as e: - print(f"Error fetching transcription from URL: {e}") - except json.JSONDecodeError as e: - print(f"Error decoding JSON from transcription URL: {e}") - except Exception as e: - print(f"An unexpected error occurred: {e}") - - def _save_segments_to_db(self, data: dict, meeting_id: int): - segments_to_insert = [] - for transcript in data.get('transcripts', []): - for sentence in transcript.get('sentences', []): - speaker_id = sentence.get('speaker_id', -1) - segments_to_insert.append(( - meeting_id, - speaker_id, # For the new speaker_id column - speaker_id, # For the speaker_tag column (initial value) - sentence.get('begin_time'), - sentence.get('end_time'), - sentence.get('text') - )) - - if not segments_to_insert: - print("No segments to save.") - return - - try: - with get_db_connection() as connection: - cursor = connection.cursor() - - # Clear existing segments for this meeting to avoid duplicates - delete_query = "DELETE FROM transcript_segments WHERE meeting_id = %s" - cursor.execute(delete_query, (meeting_id,)) - print(f"Deleted existing segments for meeting_id: {meeting_id}") - - insert_query = ''' - INSERT INTO transcript_segments (meeting_id, speaker_id, speaker_tag, start_time_ms, end_time_ms, text_content) - VALUES (%s, %s, %s, %s, %s, %s) - ''' - cursor.executemany(insert_query, segments_to_insert) - connection.commit() - print(f"Successfully saved {len(segments_to_insert)} segments to the database for meeting_id: {meeting_id}") - - except Exception as e: - print(f"Database error: {e}") - -# Main method for testing -if __name__ == '__main__': - # This is an example of how to use the service. - # You need to provide a valid meeting_id that exists in your database - # and a publicly accessible URL for the audio file. - - # Example usage: - # 1. Make sure you have a meeting with meeting_id = 1 in your database. - # 2. Make sure the audio file URL is correct and accessible. - - test_meeting_id = 40 - # Please replace with your own publicly accessible audio file URL - test_file_urls = ['http://t0vogyxkz.hn-bkt.clouddn.com/test/dajiang.m4a'] - - print("--- Running Voice Service Test ---") - voice_service = VoiceService() - voice_service.transcribe(file_urls=test_file_urls, meeting_id=test_meeting_id) - print("--- Voice Service Test Finished ---") \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index cc19d27..e97f799 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -6,6 +6,7 @@ services: ports: - "8001:8001" environment: + - TZ=Asia/Shanghai # Python运行环境 - PYTHONPATH=/app - PYTHONUNBUFFERED=1