UVa 12184 Transcribed Books

题目描述

在古腾堡发明活字印刷术之前,书籍都是由僧侣手工抄写的。各个修道院希望能够确认一本书是由他们(而不是其他修道院)抄写的。虽然水印纸是一种选择,但修道院更倾向于使用一种难以伪造的序列号系统来识别他们的抄本。

每个序列号由 101010 个数字 a1,a2,…,a10a_1, a_2, \ldots, a_{10}a1,a2,,a10 组成。有效的序列号满足:
a1+a2+…+a9≡a10 (mod N)a_1 + a_2 + \ldots + a_9 \equiv a_{10} \ (\text{mod} \ N)a1+a2++a9a10 (mod N)
其中 0≤a10<N0 \leq a_{10} < N0a10<N。这个 NNN 是特定于并仅由抄写这本书的修道院所知的,因此他们能够检查其来源。

现在你面对一堆书,这些书大概是由同一个修道院抄写的。你需要编写一个程序来确定那个修道院,即计算使得这些书的序列号都有效的最大可能的 NNN。显然,没有修道院会选择 N=1N = 1N=1。所以如果你的计算结果是 N=1N = 1N=1,那一定是出了什么问题。

输入格式

  • 第一行:整数 ttt,表示测试用例的数量(1≤t≤1001 \leq t \leq 1001t100
  • 每个测试用例:
    • 第一行:整数 ccc,表示序列号数量(2≤c≤10002 \leq c \leq 10002c1000
    • 接下来 ccc 行:每行包含 101010 个整数 a1,a2,…,a10a_1, a_2, \ldots, a_{10}a1,a2,,a100≤ai<2280 \leq a_i < 2^{28}0ai<228

输出格式

对于每个测试用例,输出最大的可能 NNN,使得所有序列号都有效。如果找不到 N>1N > 1N>1 或者序列号对任意 NNN 都有效,输出 impossible

题目分析

数学建模

对于每个序列号,定义:

  • 999 位数字之和:S=a1+a2+⋯+a9S = a_1 + a_2 + \dots + a_9S=a1+a2++a9
  • 101010 位数字:a10a_{10}a10

验证条件为:
S≡a10 (mod N)S \equiv a_{10} \ (\text{mod} \ N)Sa10 (mod N)

这等价于:
S−a10≡0 (mod N)S - a_{10} \equiv 0 \ (\text{mod} \ N)Sa100 (mod N)
即:
N∣(S−a10)N \mid (S - a_{10})N(Sa10)

关键约束条件

题目中有一个重要约束:0≤a10<N0 \leq a_{10} < N0a10<N

这意味着:

  1. a10a_{10}a10 必须小于 NNN
  2. a10a_{10}a10 是非负整数

多序列号情况

对于 ccc 个序列号,我们需要找到最大的 N>1N > 1N>1,使得:
N∣(Si−a10(i))对所有 i=1,2,…,cN \mid (S_i - a_{10}^{(i)}) \quad \text{对所有 } i = 1, 2, \ldots, cN(Sia10(i))对所有 i=1,2,,c

换句话说,NNN 必须是所有差值 di=Si−a10(i)d_i = S_i - a_{10}^{(i)}di=Sia10(i) 的公约数。

解题思路

  1. 计算差值:对于每个序列号,计算 di=(a1+⋯+a9)−a10d_i = (a_1 + \dots + a_9) - a_{10}di=(a1++a9)a10
  2. 求最大公约数:计算所有 did_idi 的绝对值的最大公约数 gcd⁡(∣d1∣,∣d2∣,…,∣dc∣)\gcd(|d_1|, |d_2|, \ldots, |d_c|)gcd(d1,d2,,dc)
  3. 特殊情况处理
    • 如果 gcd⁡=0\gcd = 0gcd=0:所有差值都为 000,序列号对任意 NNN 都有效,输出 impossible
    • 如果 gcd⁡=1\gcd = 1gcd=1:没有 N>1N > 1N>1 满足条件,输出 impossible
  4. 验证约束:检查是否满足 a10(i)<gcd⁡a_{10}^{(i)} < \gcda10(i)<gcd 对所有 iii,如果不满足则输出 impossible

算法实现细节

时间复杂度

  • 每个测试用例需要 O(c)O(c)O(c) 时间计算差值和求 gcd⁡\gcdgcd
  • 总体复杂度为 O(t⋅c)O(t \cdot c)O(tc),在题目约束下完全可行

注意事项

  1. 使用 long long 类型防止整数溢出(ai<228a_i < 2^{28}ai<228999 个数的和可能很大)
  2. 使用绝对值计算 gcd⁡\gcdgcd,避免负数影响
  3. 必须检查 a10<Na_{10} < Na10<N 的约束条件

代码实现

// Transcribed Books
// UVa ID: 12184
// Verdict: Accepted
// Submission Date: 2025-11-16
// UVa Run Time: 0.180s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

int main() {
    int t;
    cin >> t;
    while (t--) {
        int c;
        cin >> c;
        vector<long long> diffs;
        vector<long long> a10s;
        
        for (int i = 0; i < c; i++) {
            long long sum = 0;
            for (int j = 0; j < 9; j++) {
                long long x;
                cin >> x;
                sum += x;
            }
            long long a10;
            cin >> a10;
            diffs.push_back(sum - a10);
            a10s.push_back(a10);
        }
        
        long long g = 0;
        for (long long d : diffs) {
            g = __gcd(g, abs(d));
        }
        
        if (g == 0 || g == 1) {
            cout << "impossible\n";
            continue;
        }
        
        // 检查 g 是否大于所有的 a10
        bool valid = true;
        for (long long a10 : a10s) {
            if (a10 >= g) {
                valid = false;
                break;
            }
        }
        
        if (valid) {
            cout << g << "\n";
        } else {
            cout << "impossible\n";
        }
    }
    return 0;
}

样例分析

样例 1

输入:
2
1 1 1 1 1 1 1 1 1 9
2 4 6 8 10 12 14 16 18 90

计算:
序列号1: S = 9, a10 = 9, d = 0
序列号2: S = 90, a10 = 90, d = 0
gcd(0,0) = 0 → impossible

样例 2

输入:
3
1 1 1 1 1 1 1 1 1 1
5 4 7 2 6 4 2 1 3 2
1 2 3 4 5 6 7 8 9 5

计算:
序列号1: S = 9, a10 = 1, d = 8
序列号2: S = 34, a10 = 2, d = 32  
序列号3: S = 45, a10 = 5, d = 40
gcd(8,32,40) = 8
检查:所有a10 < 8 → 输出8

样例 3

输入:
2
1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 0

计算:
序列号1: S = 9, a10 = 1, d = 8
序列号2: S = 9, a10 = 0, d = 9
gcd(8,9) = 1 → impossible

样例 4

输入:
2
2 2 2 2 2 2 2 2 2 0
1 1 1 1 1 1 1 1 1 1

计算:
序列号1: S = 18, a10 = 0, d = 18
序列号2: S = 9, a10 = 1, d = 8
gcd(18,8) = 2
检查:所有a10 < 2 → 输出2

总结

本题的关键在于将模运算条件转化为整除关系,然后利用最大公约数的性质找到满足所有序列号的最大 NNN。特别需要注意 a10<Na_{10} < Na10<N 的约束条件,这是容易被忽略但至关重要的细节。

INFO: 127.0.0.1:58952 - "POST /query HTTP/1.1" 422 Unprocessable Entity 代码: from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from typing import List, Dict, Any, Optional import asyncio import logging import os import time # 添加time模块导入 import uuid # 添加uuid模块导入 from dotenv import load_dotenv import uvicorn from contextlib import asynccontextmanager # 在导入其他模块之前先加载环境变量 load_dotenv() # 导入RAG系统 from rag_main import RAGSystem, RAGConfig # 导入TTS服务 from text_to_speech import tts_service # 添加TTS服务导入 # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # 全局RAG系统实例 rag_system: Optional[RAGSystem] = None # Pydantic模型定义 class ImageData(BaseModel): """图片数据模型""" name: str = Field(..., description="图片文件名") data: str = Field(..., description="图片base64数据") class QueryRequest(BaseModel): """查询请求模型""" question: str = Field(..., description="用户问题", min_length=1, max_length=1000) include_history: bool = Field(True, description="是否包含对话历史") images: List[ImageData] = Field(default=[], description="上传的图片列表") user_id: Optional[str] = Field(None, description="用户ID") conversation_history: List[Dict[str, Any]] = Field(default=[], description="对话历史") class BatchQueryRequest(BaseModel): """批量查询请求模型""" questions: List[str] = Field(..., description="问题列表", min_items=1, max_items=10) class QueryResponse(BaseModel): """查询响应模型""" success: bool question: Optional[str] = None answer: Optional[str] = None sources: List[Dict[str, Any]] = [] context_used: Optional[int] = None retrieval_results: Optional[int] = None timing: Optional[Dict[str, float]] = None model_info: Optional[Dict[str, Any]] = None error: Optional[str] = None images: Optional[list] = [] # 新增此行,支持图片多模态返回 class SystemStatus(BaseModel): """系统状态模型""" initialized: bool chat_history_length: int files_exist: Dict[str, bool] config: Dict[str, Any] class ConfigUpdateRequest(BaseModel): """配置更新请求模型""" chunk_size: Optional[int] = Field(None, ge=100, le=2048) chunk_overlap: Optional[int] = Field(None, ge=0, le=500) retrieval_top_k: Optional[int] = Field(None, ge=1, le=50) final_top_k: Optional[int] = Field(None, ge=1, le=20) temperature: Optional[float] = Field(None, ge=0.0, le=2.0) max_tokens: Optional[int] = Field(None, ge=100, le=4096) enable_reranking: Optional[bool] = None enable_safety_check: Optional[bool] = None # 应用生命周期管理 @asynccontextmanager async def lifespan(app: FastAPI): """应用启动和关闭时的生命周期管理""" # 启动时初始化RAG系统 global rag_system try: logger.info("🚀 正在初始化RAG系统...") # 环境变量已经在模块级别加载了,这里不需要再次加载 # load_dotenv() # 删除这行 # 获取API密钥 siliconflow_api_key = os.getenv("SILICONFLOW_API_KEY") volcengine_api_key = os.getenv("VOLCENGINE_API_KEY") if not siliconflow_api_key: raise ValueError("未找到SILICONFLOW_API_KEY环境变量") if not volcengine_api_key: raise ValueError("未找到VOLCENGINE_API_KEY环境变量") # 创建配置 config = RAGConfig( api_key=siliconflow_api_key, volcengine_api_key=volcengine_api_key, document_path="招股说明书1.pdf", output_dir="./", chunk_size=384, chunk_overlap=48, retrieval_top_k=20, final_top_k=8, enable_safety_check=False ) # 初始化RAG系统 rag_system = RAGSystem(config) if not await rag_system.initialize(): raise RuntimeError("RAG系统初始化失败") # 处理文档 if not await rag_system.process_document(): raise RuntimeError("文档处理失败") logger.info("✅ RAG系统初始化成功") except Exception as e: logger.error(f"❌ RAG系统初始化失败: {e}") rag_system = None raise yield # 关闭时清理资源 logger.info("🔄 正在关闭RAG系统...") rag_system = None # 创建FastAPI应用 app = FastAPI( title="RAG智能问答API", description="基于检索增强生成(RAG)的智能问答系统API", version="1.0.0", lifespan=lifespan ) # 添加CORS中间件 app.add_middleware( CORSMiddleware, allow_origins=["*"], # 生产环境中应该限制具体域名 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 辅助函数 def check_system_ready(): """检查系统是否就绪""" if rag_system is None or not rag_system.is_initialized: raise HTTPException( status_code=503, detail="RAG系统未初始化或不可用,请稍后重试" ) # API路由定义 @app.get("/", summary="根路径", description="API根路径,返回欢迎信息") async def root(): """根路径""" return { "message": "欢迎使用RAG智能问答API", "version": "1.0.0", "docs": "/docs", "status": "/status" } @app.get("/health", summary="健康检查", description="检查API服务健康状态") async def health_check(): """健康检查""" return { "status": "healthy", "rag_system_ready": rag_system is not None and rag_system.is_initialized } @app.get("/status", response_model=SystemStatus, summary="系统状态", description="获取RAG系统详细状态") async def get_system_status(): """获取系统状态""" check_system_ready() status = rag_system.get_system_status() return SystemStatus(**status) # 在现有的Pydantic模型定义后添加 class ImageDescriptionRequest(BaseModel): """图片描述请求模型""" images: List[ImageData] = Field(..., description="要描述的图片列表", min_items=1) detailed: bool = Field(True, description="是否生成详细描述") class ImageDescriptionResponse(BaseModel): """图片描述响应模型""" success: bool descriptions: List[Dict[str, str]] = [] total_images: int processing_time: Optional[float] = None error: Optional[str] = None # 在现有的API端点后添加新的图片描述端点 @app.post("/describe_image", response_model=ImageDescriptionResponse, summary="图片描述", description="上传图片并获取AI生成的详细描述") async def describe_images(request: ImageDescriptionRequest): """图片描述接口 - 专门用于处理用户上传的图片""" import time start_time = time.time() try: from image_captioning import describe_image import base64 descriptions = [] for img in request.images: try: # 提取base64数据 if img.data.startswith('data:image/'): base64_data = img.data.split(',')[1] else: base64_data = img.data # 获取图片格式 img_format = img.name.split('.')[-1].lower() if '.' in img.name else 'png' # 生成图片描述 description = describe_image(base64_data, img_format) descriptions.append({ "image_name": img.name, "description": description, "status": "success" }) logger.info(f"✓ 成功描述图片: {img.name}") except Exception as e: logger.error(f"处理图片 {img.name} 失败: {e}") descriptions.append({ "image_name": img.name, "description": f"无法生成描述: {str(e)}", "status": "error" }) processing_time = time.time() - start_time return ImageDescriptionResponse( success=True, descriptions=descriptions, total_images=len(request.images), processing_time=processing_time ) except Exception as e: logger.error(f"图片描述处理异常: {e}") return ImageDescriptionResponse( success=False, descriptions=[], total_images=len(request.images), error=str(e) ) # 修改现有的query_rag函数,增强图片处理逻辑 @app.post("/query", response_model=QueryResponse, summary="智能问答", description="提交问题并获取AI回答") async def query_rag(request: QueryRequest): """智能问答接口""" check_system_ready() try: # 处理图片描述 image_descriptions = [] if request.images: from image_captioning import describe_image import base64 for img in request.images: try: # 添加日志以检查 img.data 和 img.name 的值 logger.info(f"处理图片: 名称={img.name}, 数据前100字符={img.data[:100]}") # 提取base64数据 if img.data.startswith('data:image/'): base64_data = img.data.split(',')[1] else: base64_data = img.data # 获取图片格式 img_format = img.name.split('.')[-1].lower() if '.' in img.name else 'png' # 生成图片描述 description = describe_image(base64_data, img_format) image_descriptions.append(f"图片 {img.name}: {description}") logger.info(f"✓ 成功处理图片: {img.name}") except Exception as e: logger.error(f"处理图片 {img.name} 失败: {e}") image_descriptions.append(f"图片 {img.name}: 无法生成描述") # 判断是否为纯图片查询(没有文字问题或问题很简单) is_image_only_query = ( request.images and (not request.question.strip() or request.question.strip().lower() in ['这是什么', '描述一下', '看看这个', '这个图片', '图片内容']) ) if is_image_only_query and image_descriptions: # 纯图片查询,直接返回图片描述 answer = "根据您上传的图片,我看到:\n\n" + "\n\n".join(image_descriptions) # 在 query_rag 方法中,构建 images 列表 images = [] for idx, img in enumerate(request.images): images.append({ "url": f"/images/{img.name}", "description": image_descriptions[idx] if idx < len(image_descriptions) else "" }) # 在 QueryResponse 返回时加入 images 字段 return QueryResponse( success=True, question=request.question, answer=answer, sources=[], images=images, context_used=0, retrieval_results=0, timing={"total_time": 0.1}, model_info={"type": "image_description_only"} ) # 构建完整问题(包含图片描述) full_question = request.question if image_descriptions: full_question += "\n\n上传的图片信息:\n" + "\n".join(image_descriptions) # 调用RAG系统 result = await rag_system.query( user_question=full_question, include_history=request.include_history ) # 保存图片描述到对话历史 if image_descriptions: result['image_descriptions'] = image_descriptions return QueryResponse(**result) except Exception as e: logger.error(f"查询处理异常: {e}") raise HTTPException( status_code=500, detail=f"查询处理失败: {str(e)}" ) @app.post("/batch_query", response_model=List[QueryResponse], summary="批量问答", description="批量提交问题并获取AI回答") async def batch_query_rag(request: BatchQueryRequest): """批量查询接口""" check_system_ready() try: results = await rag_system.batch_query(request.questions) return [QueryResponse(**result) for result in results] except Exception as e: logger.error(f"批量查询处理异常: {e}") raise HTTPException( status_code=500, detail=f"批量查询处理失败: {str(e)}" ) @app.post("/clear_history", summary="清空历史", description="清空对话历史记录") async def clear_chat_history(): """清空对话历史""" check_system_ready() try: rag_system.clear_history() return {"success": True, "message": "对话历史已清空"} except Exception as e: logger.error(f"清空历史异常: {e}") raise HTTPException( status_code=500, detail=f"清空历史失败: {str(e)}" ) @app.post("/reprocess_document", summary="重新处理文档", description="强制重新处理文档") async def reprocess_document(background_tasks: BackgroundTasks): """重新处理文档""" check_system_ready() async def process_task(): try: await rag_system.process_document(force_reprocess=True) logger.info("文档重新处理完成") except Exception as e: logger.error(f"文档重新处理失败: {e}") background_tasks.add_task(process_task) return {"success": True, "message": "文档重新处理任务已启动,请稍后查看状态"} @app.get("/config", summary="获取配置", description="获取当前系统配置") async def get_config(): """获取当前配置""" check_system_ready() config = rag_system.config return { "embedding_model": config.embedding_model, "chunk_size": config.chunk_size, "chunk_overlap": config.chunk_overlap, "retrieval_top_k": config.retrieval_top_k, "final_top_k": config.final_top_k, "generation_model": config.generation_model, "temperature": config.temperature, "max_tokens": config.max_tokens, "enable_reranking": config.enable_reranking, "enable_safety_check": config.enable_safety_check } @app.put("/config", summary="更新配置", description="更新系统配置参数") async def update_config(request: ConfigUpdateRequest): """更新配置""" check_system_ready() try: config = rag_system.config updated_fields = [] # 更新配置字段 if request.chunk_size is not None: config.chunk_size = request.chunk_size updated_fields.append("chunk_size") if request.chunk_overlap is not None: config.chunk_overlap = request.chunk_overlap updated_fields.append("chunk_overlap") if request.retrieval_top_k is not None: config.retrieval_top_k = request.retrieval_top_k updated_fields.append("retrieval_top_k") if request.final_top_k is not None: config.final_top_k = request.final_top_k updated_fields.append("final_top_k") if request.temperature is not None: config.temperature = request.temperature updated_fields.append("temperature") if request.max_tokens is not None: config.max_tokens = request.max_tokens updated_fields.append("max_tokens") if request.enable_reranking is not None: config.enable_reranking = request.enable_reranking updated_fields.append("enable_reranking") if request.enable_safety_check is not None: config.enable_safety_check = request.enable_safety_check updated_fields.append("enable_safety_check") return { "success": True, "message": f"配置已更新: {', '.join(updated_fields)}", "updated_fields": updated_fields } except Exception as e: logger.error(f"配置更新异常: {e}") raise HTTPException( status_code=500, detail=f"配置更新失败: {str(e)}" ) # 错误处理 @app.exception_handler(Exception) async def global_exception_handler(request, exc): """全局异常处理""" logger.error(f"未处理的异常: {exc}") if isinstance(exc, HTTPException): return JSONResponse(status_code=exc.status_code, content={ "success": False, "error": exc.detail }) return { "success": False, "error": "服务器内部错误", "detail": str(exc) } # 主函数 import os if __name__ == "__main__": # 开发环境启用重载,生产环境禁用 reload_mode = os.getenv("UVICORN_RELOAD", "true").lower() == "true" uvicorn.run( "rag_api:app", host="0.0.0.0", port=8000, reload=reload_mode, log_level="info" ) from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles # 添加静态文件服务 app.mount("/images", StaticFiles(directory="images"), name="images") @app.get("/image/{image_name}") async def get_image(image_name: str): """获取图片文件""" image_path = os.path.join("images", image_name) if os.path.exists(image_path): return FileResponse(image_path) else: raise HTTPException(status_code=404, detail="图片未找到") from speech_to_text import process_speech_to_text from fastapi import UploadFile, File # 添加语音转文字接口 @app.post("/speech-to-text") async def speech_to_text(audio_file: UploadFile = File(...)): """语音转文字接口""" try: # 验证文件类型 if not audio_file.content_type.startswith('audio/'): raise HTTPException(status_code=400, detail="请上传音频文件") # 处理语音转文字 result = await process_speech_to_text(audio_file) return JSONResponse(content=result) except Exception as e: logger.error(f"语音转文字API错误: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/voice-query") async def voice_query(audio_file: UploadFile = File(...)): """语音查询接口:语音转文字 + RAG查询""" try: # 1. 语音转文字 speech_result = await process_speech_to_text(audio_file) if not speech_result["success"]: return JSONResponse(content=speech_result) transcribed_text = speech_result["transcribed_text"] # 添加调试日志 logger.info(f"原始转录结果: {speech_result['transcribed_text']}") logger.info(f"转录文本类型: {type(transcribed_text)}") # 如果转录结果是列表格式,需要提取文本 if isinstance(transcribed_text, list) and len(transcribed_text) > 0: if isinstance(transcribed_text[0], dict) and 'text' in transcribed_text[0]: transcribed_text = transcribed_text[0]['text'] elif isinstance(transcribed_text[0], str): transcribed_text = transcribed_text[0] logger.info(f"处理后的查询文本: {transcribed_text}") # 2. 使用转录文本进行RAG查询 if hasattr(rag_system, 'query_with_images'): query_result = await rag_system.query_with_images(transcribed_text) else: query_result = await rag_system.query(transcribed_text) # 3. 返回完整结果 return JSONResponse(content={ "success": True, "transcribed_text": transcribed_text, "audio_file_path": speech_result["audio_file_path"], "answer": query_result["answer"], "sources": query_result["sources"], "images": query_result.get("images", []), "processing_time": query_result.get("timing", {}).get("total_time", 0) }) except Exception as e: logger.error(f"语音查询API错误: {e}") @app.post("/query-with-tts") async def query_with_tts(request: QueryRequest): """ 智能问答并返回TTS音频 """ check_system_ready() try: start_time = time.time() # 执行RAG查询 result = await rag_system.query( user_question=request.question, include_history=request.include_history ) if not result.get('success', False): raise HTTPException(status_code=500, detail=result.get('error', '查询失败')) answer_text = result.get('answer', '') # 尝试生成TTS音频(增强错误处理) try: # 检查方法是否存在 if hasattr(tts_service, 'text_to_speech'): audio_data = tts_service.text_to_speech(answer_text) elif hasattr(tts_service, 'text_to_speech_stream'): # 使用流式方法作为备选 audio_stream = tts_service.text_to_speech_stream(answer_text) if audio_stream: # 处理流式数据 audio_data = b''.join(audio_stream) else: audio_data = None else: logger.error("No available TTS method found") audio_data = None except Exception as e: logger.error(f"TTS service error: {str(e)}") audio_data = None if audio_data: # 创建临时文件保存音频 audio_filename = f"audio_{uuid.uuid4().hex}.wav" audio_path = os.path.join("audio_files", audio_filename) # 确保audio_files目录存在 os.makedirs("audio_files", exist_ok=True) # 保存音频文件 if tts_service.save_audio_to_file(audio_data, audio_path): result['audio_file'] = audio_filename result['audio_url'] = f"/audio/{audio_filename}" logger.info(f"✓ TTS音频生成成功: {audio_filename}") else: logger.warning("音频文件保存失败") result['tts_error'] = "音频文件保存失败" else: logger.warning("TTS转换失败") result['tts_error'] = "TTS转换失败" processing_time = time.time() - start_time result['processing_time'] = processing_time return QueryResponse(**result) except HTTPException: raise except Exception as e: logger.error(f"Query with TTS error: {str(e)}") raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") # 添加音频文件服务端点 @app.get("/audio/{audio_filename}") async def get_audio(audio_filename: str): """ 获取生成的音频文件 """ audio_path = os.path.join("audio_files", audio_filename) if not os.path.exists(audio_path): raise HTTPException(status_code=404, detail="音频文件不存在") return FileResponse( audio_path, media_type="audio/wav", filename=audio_filename ) # 添加TTS独立端点 @app.post("/text-to-speech") async def text_to_speech_endpoint(text: str, voice: str = "Cherry"): """ 独立的文字转语音端点 """ try: audio_data = tts_service.text_to_speech_stream(text, voice) if not audio_data: raise HTTPException(status_code=500, detail="TTS转换失败") # 创建临时文件 audio_filename = f"tts_{uuid.uuid4().hex}.wav" audio_path = os.path.join("audio_files", audio_filename) # 确保目录存在 os.makedirs("audio_files", exist_ok=True) # 保存音频文件 if tts_service.save_audio_to_file(audio_data, audio_path): return { "success": True, "audio_file": audio_filename, "audio_url": f"/audio/{audio_filename}" } else: raise HTTPException(status_code=500, detail="音频文件保存失败") except Exception as e: logger.error(f"TTS endpoint error: {str(e)}") raise HTTPException(status_code=500, detail=f"TTS转换失败: {str(e)}") def build_multimodal_answer(answer_text, images): multimodal_part = "" for idx, img in enumerate(images, 1): multimodal_part += f"如图{idx}所示:{img['description']}\n【图片{idx}:{img['url']}, {img['description']}】\n" return answer_text + "\n" + multimodal_part if images else answer_text 检查代码错误
08-22
你的 FastAPI 项目整体结构清晰,功能完整,涵盖了 RAG 查询、图像描述、语音识别、TTS、配置管理、文件服务等多个模块。不过你遇到的错误: ``` INFO: 127.0.0.1:58952 - "POST /query HTTP/1.1" 422 Unprocessable Entity ``` 是一个 **请求体验证失败** 的错误,通常是由于请求体与定义的 Pydantic 模型不匹配导致的。我们来逐步分析这个问题。 --- ### 🔍 错误解析 `422 Unprocessable Entity` 是 FastAPI 返回的一种验证错误,表示客户端提交的数据格式不正确,**无法通过 Pydantic 模型的验证规则**。 --- ### ✅ 问题定位 你定义了 `QueryRequest` 模型如下: ```python class QueryRequest(BaseModel): question: str = Field(..., description="用户问题", min_length=1, max_length=1000) include_history: bool = Field(True, description="是否包含对话历史") images: List[ImageData] = Field(default=[], description="上传的图片列表") user_id: Optional[str] = Field(None, description="用户ID") conversation_history: List[Dict[str, Any]] = Field(default=[], description="对话历史") ``` FastAPI 会严格按照这个模型来验证请求体。如果请求体中字段名不匹配、类型错误、缺少必填字段(如 `question`)、字段值不满足约束(如长度超过 1000),就会导致 422 错误。 --- ### 🧪 常见错误原因 1. **字段名不一致** - 请求体中的字段名与模型定义不一致,例如 `question` 写成 `Question` 或 `query`。 2. **字段类型错误** - `include_history` 应该是布尔值,如果传入 `"true"`(字符串)或 `1`(整数),就会验证失败。 3. **缺少必填字段** - `question` 是必填项,如果请求体中没有提供,就会报错。 4. **字段值不符合约束** - `question` 长度超过 `max_length=1000`,也会导致验证失败。 5. **嵌套字段格式错误** - `images` 字段是 `List[ImageData]` 类型,每个元素必须是 `ImageData` 模型实例,如果格式不对(如缺少 `name` 或 `data` 字段)也会失败。 --- ### 🛠 示例错误请求体 ```json { "Question": "这是一道问题", // 错误:字段名应该是 question "include_history": "true", // 错误:应该是布尔值 "images": [ { "name": "image1.png", "data": "base64_data" // 如果 base64 数据不正确,也可能出错 } ], "conversation_history": [ { "role": "user", "content": "你好" // content 应该是 Dict 类型?还是 str?需要看实际定义 } ] } ``` --- ### ✅ 解决方案 #### ✅ 1. 确保请求体字段名一致 ```json { "question": "这是一个问题", "include_history": true, "images": [ { "name": "image1.png", "data": "base64_encoded_string" } ], "user_id": "user_123", "conversation_history": [ { "role": "user", "content": "你好" }, { "role": "assistant", "content": "你好!有什么我可以帮助你的吗?" } ] } ``` #### ✅ 2. 使用 `curl` 或 Postman 验证请求 ```bash curl -X POST "http://localhost:8000/query" \ -H "Content-Type: application/json" \ -d '{ "question": "这是一个问题", "include_history": true, "images": [ { "name": "image1.png", "data": "base64_encoded_string" } ], "user_id": "user_123", "conversation_history": [] }' ``` #### ✅ 3. 使用 Swagger UI 测试 访问 `http://localhost:8000/docs`,找到 `/query` 接口,使用提供的表单测试接口,确保输入格式正确。 --- ### ✅ 优化建议 - **添加详细的日志记录**:在 `/query` 接口中添加请求体日志输出,方便调试。 - **使用 `ValidationError` 捕获**:可以在全局异常处理器中捕获 `ValidationError`,返回更清晰的错误信息。 - **使用 `@app.exception_handler(RequestValidationError)`**:自定义请求验证错误响应。 ```python from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): logger.error(f"请求验证失败: {exc}") return JSONResponse( status_code=422, content={ "success": False, "error": "请求格式错误", "detail": exc.errors() } ) ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值