diff --git a/app.zip b/app.zip index 7953381..ea49c8b 100644 Binary files a/app.zip and b/app.zip differ diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py index 1def460..a6c507e 100644 --- a/app/api/endpoints/meetings.py +++ b/app/api/endpoints/meetings.py @@ -1,5 +1,6 @@ + from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Depends, BackgroundTasks -from app.models.models import Meeting, TranscriptSegment, TranscriptionTaskStatus, CreateMeetingRequest, UpdateMeetingRequest, SpeakerTagUpdateRequest, BatchSpeakerTagUpdateRequest, TranscriptUpdateRequest, BatchTranscriptUpdateRequest +from app.models.models import Meeting, TranscriptSegment, TranscriptionTaskStatus, CreateMeetingRequest, UpdateMeetingRequest, SpeakerTagUpdateRequest, BatchSpeakerTagUpdateRequest, TranscriptUpdateRequest, BatchTranscriptUpdateRequest, Tag from app.core.database import get_db_connection from app.core.config import BASE_DIR, UPLOAD_DIR, AUDIO_DIR, MARKDOWN_DIR, ALLOWED_EXTENSIONS, ALLOWED_IMAGE_EXTENSIONS, MAX_FILE_SIZE, MAX_IMAGE_SIZE from app.services.qiniu_service import qiniu_service @@ -7,7 +8,7 @@ from app.services.llm_service import LLMService from app.services.async_transcription_service import AsyncTranscriptionService from app.services.async_llm_service import async_llm_service from app.core.auth import get_current_user, get_optional_current_user -from typing import Optional +from typing import List, Optional from datetime import datetime from pydantic import BaseModel import os @@ -27,6 +28,24 @@ transcription_service = AsyncTranscriptionService() class GenerateSummaryRequest(BaseModel): user_prompt: Optional[str] = "" +def _process_tags(cursor, tag_string: Optional[str]) -> List[Tag]: + if not tag_string: + return [] + + tag_names = [name.strip() for name in tag_string.split(',') if name.strip()] + if not tag_names: + return [] + + # Ensure all tags exist in the 'tags' table + insert_ignore_query = "INSERT IGNORE INTO tags (name) VALUES (%s)" + cursor.executemany(insert_ignore_query, [(name,) for name in tag_names]) + + # Fetch the full tag objects + format_strings = ', '.join(['%s'] * len(tag_names)) + cursor.execute(f"SELECT id, name, color FROM tags WHERE name IN ({format_strings})", tuple(tag_names)) + tags_data = cursor.fetchall() + return [Tag(**tag) for tag in tags_data] + @router.get("/meetings", response_model=list[Meeting]) def get_meetings(current_user: dict = Depends(get_current_user), user_id: Optional[int] = None): with get_db_connection() as connection: @@ -34,7 +53,7 @@ def get_meetings(current_user: dict = Depends(get_current_user), user_id: Option base_query = ''' SELECT - m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, m.user_id as creator_id, u.caption as creator_username FROM meetings m JOIN users u ON m.user_id = u.user_id @@ -67,6 +86,8 @@ def get_meetings(current_user: dict = Depends(get_current_user), user_id: Option attendees_data = cursor.fetchall() attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + tags = _process_tags(cursor, meeting.get('tags')) + meeting_list.append(Meeting( meeting_id=meeting['meeting_id'], title=meeting['title'], @@ -75,7 +96,8 @@ def get_meetings(current_user: dict = Depends(get_current_user), user_id: Option created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], - creator_username=meeting['creator_username'] + creator_username=meeting['creator_username'], + tags=tags )) return meeting_list @@ -87,7 +109,7 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren query = ''' SELECT - m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, m.user_id as creator_id, u.caption as creator_username, af.file_path as audio_file_path FROM meetings m @@ -112,6 +134,8 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren attendees_data = cursor.fetchall() attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + tags = _process_tags(cursor, meeting.get('tags')) + # 关闭游标,避免与转录服务的数据库连接冲突 cursor.close() @@ -123,7 +147,8 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], - creator_username=meeting['creator_username'] + creator_username=meeting['creator_username'], + tags=tags ) # Add audio file path if exists @@ -182,16 +207,24 @@ def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict = D with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) + # Process tags + if meeting_request.tags: + tag_names = [name.strip() for name in meeting_request.tags.split(',') if name.strip()] + if tag_names: + insert_ignore_query = "INSERT IGNORE INTO tags (name) VALUES (%s)" + cursor.executemany(insert_ignore_query, [(name,) for name in tag_names]) + # Create meeting meeting_query = ''' - INSERT INTO meetings (user_id, title, meeting_time, summary,created_at) - VALUES (%s, %s, %s, %s, %s) + INSERT INTO meetings (user_id, title, meeting_time, summary, tags, created_at) + VALUES (%s, %s, %s, %s, %s, %s) ''' cursor.execute(meeting_query, ( meeting_request.user_id, meeting_request.title, meeting_request.meeting_time, None, + meeting_request.tags, datetime.now().isoformat() )) @@ -221,17 +254,25 @@ def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, curre if meeting['user_id'] != current_user['user_id']: raise HTTPException(status_code=403, detail="Permission denied") + + # Process tags + if meeting_request.tags: + tag_names = [name.strip() for name in meeting_request.tags.split(',') if name.strip()] + if tag_names: + insert_ignore_query = "INSERT IGNORE INTO tags (name) VALUES (%s)" + cursor.executemany(insert_ignore_query, [(name,) for name in tag_names]) # Update meeting update_query = ''' UPDATE meetings - SET title = %s, meeting_time = %s, summary = %s + SET title = %s, meeting_time = %s, summary = %s, tags = %s WHERE meeting_id = %s ''' cursor.execute(update_query, ( meeting_request.title, meeting_request.meeting_time, meeting_request.summary, + meeting_request.tags, meeting_id )) @@ -282,7 +323,7 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre query = ''' SELECT - m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, m.user_id as creator_id, u.caption as creator_username, af.file_path as audio_file_path FROM meetings m @@ -308,6 +349,8 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre attendees_data = cursor.fetchall() attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + tags = _process_tags(cursor, meeting.get('tags')) + # 关闭游标,避免与转录服务的数据库连接冲突 cursor.close() @@ -319,7 +362,8 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], - creator_username=meeting['creator_username'] + creator_username=meeting['creator_username'], + tags=tags ) # Add audio file path if exists @@ -906,4 +950,4 @@ def get_meeting_llm_tasks(meeting_id: int, current_user: dict = Depends(get_curr # } # except Exception as e: -# raise HTTPException(status_code=500, detail=f"Failed to get latest LLM task: {str(e)}") \ No newline at end of file +# raise HTTPException(status_code=500, detail=f"Failed to get latest LLM task: {str(e)}") diff --git a/app/api/endpoints/tags.py b/app/api/endpoints/tags.py new file mode 100644 index 0000000..fffd419 --- /dev/null +++ b/app/api/endpoints/tags.py @@ -0,0 +1,45 @@ + +from fastapi import APIRouter, HTTPException, Depends +from app.core.database import get_db_connection +from app.models.models import Tag +from typing import List +import mysql.connector + +router = APIRouter() + +@router.get("/tags/", response_model=List[Tag]) +def get_all_tags(): + """_summary_ + 获取所有标签 + """ + query = "SELECT id, name, color FROM tags ORDER BY name" + try: + with get_db_connection() as connection: + with connection.cursor(dictionary=True) as cursor: + cursor.execute(query) + tags = cursor.fetchall() + return tags + except mysql.connector.Error as err: + print(f"Error: {err}") + raise HTTPException(status_code=500, detail="Failed to retrieve tags from database.") + +@router.post("/tags/", response_model=Tag) +def create_tag(tag_in: Tag): + """_summary_ + 创建一个新标签 + """ + query = "INSERT INTO tags (name, color) VALUES (%s, %s)" + try: + with get_db_connection() as connection: + with connection.cursor(dictionary=True) as cursor: + try: + cursor.execute(query, (tag_in.name, tag_in.color)) + connection.commit() + tag_id = cursor.lastrowid + return {"id": tag_id, "name": tag_in.name, "color": tag_in.color} + except mysql.connector.IntegrityError: + connection.rollback() + raise HTTPException(status_code=400, detail=f"Tag '{tag_in.name}' already exists.") + except mysql.connector.Error as err: + print(f"Error: {err}") + raise HTTPException(status_code=500, detail="Failed to create tag in database.") diff --git a/app/models/models.py b/app/models/models.py index 5a2a808..6669d47 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,4 +1,3 @@ - from pydantic import BaseModel, EmailStr from typing import Optional, Union, List import datetime @@ -51,6 +50,11 @@ class AttendeeInfo(BaseModel): user_id: int caption: str +class Tag(BaseModel): + id: int + name: str + color: str + class TranscriptionTaskStatus(BaseModel): task_id: str status: str # 'pending', 'processing', 'completed', 'failed' @@ -72,6 +76,7 @@ class Meeting(BaseModel): creator_username: str audio_file_path: Optional[str] = None transcription_status: Optional[TranscriptionTaskStatus] = None + tags: Optional[List[Tag]] = [] class TranscriptSegment(BaseModel): segment_id: int @@ -87,12 +92,14 @@ class CreateMeetingRequest(BaseModel): title: str meeting_time: Optional[datetime.datetime] attendee_ids: list[int] + tags: Optional[str] = None class UpdateMeetingRequest(BaseModel): title: str meeting_time: Optional[datetime.datetime] summary: Optional[str] attendee_ids: list[int] + tags: Optional[str] = None class SpeakerTagUpdateRequest(BaseModel): speaker_id: int # 使用原始speaker_id(整数) @@ -110,4 +117,4 @@ class BatchTranscriptUpdateRequest(BaseModel): class PasswordChangeRequest(BaseModel): old_password: str - new_password: str + new_password: str \ No newline at end of file diff --git a/main.py b/main.py index 8d48397..8ef5fc9 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ import uvicorn from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from app.api.endpoints import auth, users, meetings +from app.api.endpoints import auth, users, meetings, tags from app.core.config import UPLOAD_DIR, API_CONFIG, MAX_FILE_SIZE from app.services.async_llm_service import async_llm_service import os @@ -30,6 +30,7 @@ if UPLOAD_DIR.exists(): app.include_router(auth.router, prefix="/api", tags=["Authentication"]) app.include_router(users.router, prefix="/api", tags=["Users"]) app.include_router(meetings.router, prefix="/api", tags=["Meetings"]) +app.include_router(tags.router, prefix="/api", tags=["Tags"]) @app.get("/") def read_root():