Compare commits

...

2 Commits

Author SHA1 Message Date
jeremygan2021
37b2cf6ba6 AI图片 2026-03-02 12:32:53 +08:00
jeremygan2021
82bba110ee AI图片 2026-03-02 12:32:45 +08:00
25 changed files with 254 additions and 2 deletions

2
.env
View File

@@ -4,6 +4,8 @@
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna # DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f
# MQTT配置 # MQTT配置
MQTT_BROKER_HOST=luna-mqtt MQTT_BROKER_HOST=luna-mqtt
MQTT_BROKER_PORT=1883 MQTT_BROKER_PORT=1883

View File

@@ -9,6 +9,8 @@ MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025 MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021 MQTT_PASSWORD=123luna2021
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f
# 应用配置 # 应用配置
APP_NAME=墨水屏桌面屏幕系统 APP_NAME=墨水屏桌面屏幕系统
DEBUG=false DEBUG=false

View File

@@ -9,6 +9,10 @@ MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025 MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021 MQTT_PASSWORD=123luna2021
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f
# 应用配置 # 应用配置
APP_NAME=墨水屏桌面屏幕系统 APP_NAME=墨水屏桌面屏幕系统
DEBUG=false DEBUG=false

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter from fastapi import APIRouter
from api import devices, contents, todos from api import devices, contents, todos, ai
api_router = APIRouter() api_router = APIRouter()
@@ -7,3 +7,4 @@ api_router = APIRouter()
api_router.include_router(devices.router, prefix="/devices") api_router.include_router(devices.router, prefix="/devices")
api_router.include_router(contents.router, prefix="/contents") api_router.include_router(contents.router, prefix="/contents")
api_router.include_router(todos.router, prefix="/todos") api_router.include_router(todos.router, prefix="/todos")
api_router.include_router(ai.router, prefix="/ai", tags=["AI生成"])

Binary file not shown.

Binary file not shown.

179
api/ai.py Normal file
View File

@@ -0,0 +1,179 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import Dict, Any
import httpx
import json
import logging
from config import settings
from api.ai_schemas import AIGenerationRequest, AITaskResponse, AITaskResult, AITemplateGenerationRequest
from api.prompts import get_prompt
router = APIRouter()
logger = logging.getLogger(__name__)
DASHSCOPE_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
DASHSCOPE_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks"
async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n: int, model: str):
if not settings.dashscope_api_key:
raise HTTPException(status_code=500, detail="DashScope API Key not configured")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {settings.dashscope_api_key}",
"X-DashScope-Async": "enable" # 确保异步任务提交
}
# 构建请求体
payload = {
"model": model,
"input": {
"messages": [
{
"role": "user",
"content": [
{
"text": prompt
}
]
}
]
},
"parameters": {
"prompt_extend": True,
"watermark": False,
"n": n,
"size": size
}
}
if negative_prompt:
payload["parameters"]["negative_prompt"] = negative_prompt
try:
async with httpx.AsyncClient() as client:
response = await client.post(
DASHSCOPE_API_URL,
headers=headers,
json=payload,
timeout=30.0
)
if response.status_code != 200:
logger.error(f"DashScope API error: {response.text}")
try:
error_detail = response.json()
except:
error_detail = {"message": response.text}
raise HTTPException(status_code=response.status_code, detail=error_detail)
result = response.json()
if "output" in result and "task_id" in result["output"]:
task_id = result["output"]["task_id"]
elif "task_id" in result:
task_id = result["task_id"]
else:
if "output" in result and "task_id" in result["output"]:
task_id = result["output"]["task_id"]
else:
logger.warning(f"Unexpected response structure: {result}")
task_id = result.get("output", {}).get("task_id")
if not task_id:
raise HTTPException(status_code=500, detail="Failed to retrieve task_id from DashScope response")
return AITaskResponse(
task_id=task_id,
request_id=result.get("request_id")
)
except httpx.RequestError as e:
logger.error(f"Request error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")
@router.post("/generate", response_model=AITaskResponse, summary="提交AI图片生成任务")
async def generate_image(request: AIGenerationRequest):
"""
提交AI图片生成任务使用阿里云DashScope服务
"""
return await _submit_dashscope_task(
prompt=request.prompt,
negative_prompt=request.negative_prompt,
size=request.size,
n=request.n,
model=request.model
)
@router.post("/generate_from_template", response_model=AITaskResponse, summary="使用模板提交AI图片生成任务")
async def generate_image_from_template(request: AITemplateGenerationRequest):
"""
使用预设模板提交AI图片生成任务
"""
try:
prompt = get_prompt(request.template_id, **request.params)
if prompt is None:
raise HTTPException(status_code=404, detail=f"未找到模板ID: {request.template_id}")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return await _submit_dashscope_task(
prompt=prompt,
negative_prompt=request.negative_prompt,
size=request.size,
n=request.n,
model=request.model
)
@router.get("/tasks/{task_id}", response_model=AITaskResult, summary="查询AI任务结果")
async def get_task_result(task_id: str):
"""
查询AI生成任务的结果
"""
if not settings.dashscope_api_key:
raise HTTPException(status_code=500, detail="DashScope API Key not configured")
headers = {
"Authorization": f"Bearer {settings.dashscope_api_key}"
}
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{DASHSCOPE_TASK_URL}/{task_id}",
headers=headers,
timeout=30.0
)
if response.status_code != 200:
logger.error(f"DashScope Task API error: {response.text}")
raise HTTPException(status_code=response.status_code, detail="Failed to fetch task status")
result = response.json()
# 构建返回结果
# DashScope 任务查询返回结构:
# {
# "request_id": "...",
# "output": {
# "task_id": "...",
# "task_status": "SUCCEEDED",
# "results": [...]
# },
# "usage": ...
# }
task_status = result.get("output", {}).get("task_status", "UNKNOWN")
return AITaskResult(
task_id=task_id,
status=task_status,
code=result.get("code"),
message=result.get("message"),
output=result.get("output"),
usage=result.get("usage")
)
except httpx.RequestError as e:
logger.error(f"Request error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")

30
api/ai_schemas.py Normal file
View File

@@ -0,0 +1,30 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
# AI生成相关模型
class AIGenerationRequest(BaseModel):
prompt: str = Field(..., description="生成图片的提示词")
negative_prompt: Optional[str] = Field(None, description="反向提示词")
size: str = Field("1024*1024", description="图片尺寸")
n: int = Field(1, description="生成数量", ge=1, le=4)
model: str = Field("wan2.6-t2i", description="使用的模型")
class AITemplateGenerationRequest(BaseModel):
template_id: str = Field(..., description="提示词模板ID")
params: Dict[str, str] = Field(default_factory=dict, description="提示词参数")
negative_prompt: Optional[str] = Field(None, description="反向提示词")
size: str = Field("1024*1024", description="图片尺寸")
n: int = Field(1, description="生成数量", ge=1, le=4)
model: str = Field("wan2.6-t2i", description="使用的模型")
class AITaskResponse(BaseModel):
task_id: str = Field(..., description="任务ID")
request_id: Optional[str] = Field(None, description="请求ID")
class AITaskResult(BaseModel):
task_id: str
status: str
code: Optional[str] = None
message: Optional[str] = None
output: Optional[Dict[str, Any]] = None
usage: Optional[Dict[str, Any]] = None

31
api/prompts.py Normal file
View File

@@ -0,0 +1,31 @@
# 预设提示词模板库
# 使用Python的format语法进行参数替换例如 {keyword}
PROMPTS = {
"flower_shop": "一间有着精致窗户的花店,漂亮的木质门,摆放着{flower_type},风格为{style}",
"landscape": "宏伟的{season}自然风景,包含{element},光线为{lighting},高分辨率,写实风格",
"cyberpunk_city": "赛博朋克风格的未来城市,霓虹灯闪烁,{weather}天气,街道上有{vehicle}",
"portrait": "一张{gender}的肖像照,{expression}表情,背景是{background},专业摄影布光",
"default": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵"
}
def get_prompt(template_id: str, **kwargs) -> str:
"""
获取并格式化提示词
:param template_id: 提示词模板ID
:param kwargs: 替换参数
:return: 格式化后的提示词
"""
template = PROMPTS.get(template_id)
if not template:
return None
try:
# 使用 safe_substitute 避免参数缺失报错?
# Python的format如果缺参数会报错这里直接用format让调用者负责提供完整参数
# 或者我们可以在这里处理默认值
return template.format(**kwargs)
except KeyError as e:
# 如果缺少参数,抛出异常或返回包含未替换占位符的字符串
# 这里为了简单,如果出错,我们尽量保留原样或者报错
raise ValueError(f"缺少提示词参数: {e}")

View File

@@ -35,6 +35,9 @@ class Settings(BaseSettings):
admin_username: str = "admin" admin_username: str = "admin"
admin_password: str = "123456" admin_password: str = "123456"
# DashScope配置
dashscope_api_key: Optional[str] = None
class Config: class Config:
env_file = ".env" env_file = ".env"
case_sensitive = False case_sensitive = False