Add 电子吧唧
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 7m15s
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 7m15s
This commit is contained in:
parent
51a673e814
commit
fe0dcb78c3
0
apps/badge/__init__.py
Normal file
0
apps/badge/__init__.py
Normal file
10
apps/badge/admin.py
Normal file
10
apps/badge/admin.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import BadgeImage
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(BadgeImage)
|
||||||
|
class BadgeImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'user', 'source', 'style', 'generation_status', 'created_at']
|
||||||
|
list_filter = ['source', 'generation_status', 'style']
|
||||||
|
search_fields = ['prompt', 'user__nickname']
|
||||||
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
7
apps/badge/apps.py
Normal file
7
apps/badge/apps.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.badge'
|
||||||
|
verbose_name = '电子吧唧'
|
||||||
35
apps/badge/migrations/0001_initial.py
Normal file
35
apps/badge/migrations/0001_initial.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BadgeImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('prompt', models.TextField(blank=True, default='')),
|
||||||
|
('style', models.CharField(blank=True, default='', max_length=32)),
|
||||||
|
('source', models.CharField(choices=[('t2i', '文生图'), ('i2i', '图生图'), ('upload', '用户上传')], default='t2i', max_length=10)),
|
||||||
|
('image_url', models.URLField(blank=True, default='', max_length=500)),
|
||||||
|
('reference_image_url', models.URLField(blank=True, default='', max_length=500)),
|
||||||
|
('strength', models.FloatField(default=0.7)),
|
||||||
|
('generation_status', models.CharField(choices=[('pending', '待生成'), ('generating', '生成中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_images', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'badge_image',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/badge/migrations/__init__.py
Normal file
0
apps/badge/migrations/__init__.py
Normal file
42
apps/badge/models.py
Normal file
42
apps/badge/models.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
电子吧唧 - 数据模型
|
||||||
|
"""
|
||||||
|
from django.db import models
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeImage(models.Model):
|
||||||
|
"""吧唧图片记录"""
|
||||||
|
|
||||||
|
SOURCE_CHOICES = [
|
||||||
|
('t2i', '文生图'),
|
||||||
|
('i2i', '图生图'),
|
||||||
|
('upload', '用户上传'),
|
||||||
|
]
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('pending', '待生成'),
|
||||||
|
('generating', '生成中'),
|
||||||
|
('completed', '已完成'),
|
||||||
|
('failed', '失败'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='badge_images')
|
||||||
|
prompt = models.TextField(blank=True, default='')
|
||||||
|
style = models.CharField(max_length=32, blank=True, default='')
|
||||||
|
source = models.CharField(max_length=10, choices=SOURCE_CHOICES, default='t2i')
|
||||||
|
image_url = models.URLField(max_length=500, blank=True, default='')
|
||||||
|
reference_image_url = models.URLField(max_length=500, blank=True, default='')
|
||||||
|
strength = models.FloatField(default=0.7)
|
||||||
|
generation_status = models.CharField(
|
||||||
|
max_length=20, choices=STATUS_CHOICES, default='pending'
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'badge_image'
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'BadgeImage#{self.id} [{self.source}] {self.prompt[:30]}'
|
||||||
31
apps/badge/serializers.py
Normal file
31
apps/badge/serializers.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
电子吧唧 - 序列化器
|
||||||
|
"""
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import BadgeImage
|
||||||
|
|
||||||
|
VALID_STYLES = [
|
||||||
|
'anime', 'realistic', 'pixel', 'watercolor',
|
||||||
|
'cyberpunk', 'cute', 'ink', 'comic',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeImageSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BadgeImage
|
||||||
|
fields = [
|
||||||
|
'id', 'prompt', 'style', 'source', 'image_url',
|
||||||
|
'generation_status', 'created_at',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Text2ImageSerializer(serializers.Serializer):
|
||||||
|
prompt = serializers.CharField(max_length=500)
|
||||||
|
style = serializers.ChoiceField(choices=VALID_STYLES, required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Image2ImageSerializer(serializers.Serializer):
|
||||||
|
image = serializers.ImageField()
|
||||||
|
prompt = serializers.CharField(max_length=500, required=False, allow_blank=True, default='')
|
||||||
|
style = serializers.ChoiceField(choices=VALID_STYLES, required=False)
|
||||||
|
strength = serializers.FloatField(default=0.7, min_value=0.1, max_value=1.0, required=False)
|
||||||
0
apps/badge/services/__init__.py
Normal file
0
apps/badge/services/__init__.py
Normal file
244
apps/badge/services/badge_image_service.py
Normal file
244
apps/badge/services/badge_image_service.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
"""
|
||||||
|
电子吧唧 - AI 图片生成服务
|
||||||
|
使用火山引擎豆包 Seedream 文生图模型,与故事封面生成共用同一模型。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import requests as req_lib
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 风格 → 提示词后缀映射
|
||||||
|
STYLE_PROMPT_MAP = {
|
||||||
|
'anime': '日系动漫风格,精致细腻,色彩鲜明',
|
||||||
|
'realistic': '超写实风格,高清摄影质感,细节丰富',
|
||||||
|
'pixel': '像素艺术风格,复古游戏画面,8-bit色彩',
|
||||||
|
'watercolor': '水彩画风格,淡雅柔和,笔触自然晕染',
|
||||||
|
'cyberpunk': '赛博朋克风格,霓虹灯光,暗色调科幻感',
|
||||||
|
'cute': '可爱卡通风格,Q版萌系,圆润造型,柔和配色',
|
||||||
|
'ink': '中国水墨画风格,黑白灰韵,留白意境',
|
||||||
|
'comic': '漫画风格,粗线条,强对比,夸张表现力',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sse_event(data: dict) -> str:
|
||||||
|
"""格式化 SSE data 行"""
|
||||||
|
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_t2i_stream(user, badge_image, prompt, style=None, width=1920, height=1920):
|
||||||
|
"""
|
||||||
|
文生图 SSE 流。
|
||||||
|
使用豆包 Seedream 模型生成正方形图片,上传 OSS,返回 URL。
|
||||||
|
"""
|
||||||
|
config = settings.LLM_CONFIG
|
||||||
|
|
||||||
|
if not config.get('API_KEY'):
|
||||||
|
badge_image.generation_status = 'failed'
|
||||||
|
badge_image.save(update_fields=['generation_status'])
|
||||||
|
yield sse_event({'stage': 'error', 'message': 'AI 服务未配置'})
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from volcenginesdkarkruntime import Ark
|
||||||
|
except ImportError:
|
||||||
|
badge_image.generation_status = 'failed'
|
||||||
|
badge_image.save(update_fields=['generation_status'])
|
||||||
|
yield sse_event({'stage': 'error', 'message': 'AI SDK 未安装'})
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Stage 1: 生成中 ──
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'generating', 'progress': 20,
|
||||||
|
'message': '正在生成图片...',
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = Ark(api_key=config['API_KEY'])
|
||||||
|
|
||||||
|
# 构建提示词
|
||||||
|
full_prompt = _build_prompt(prompt, style)
|
||||||
|
image_model = config.get('IMAGE_MODEL_NAME', 'doubao-seedream-4-5-251128')
|
||||||
|
image_size = f'{width}x{height}'
|
||||||
|
|
||||||
|
result = client.images.generate(
|
||||||
|
model=image_model,
|
||||||
|
prompt=full_prompt,
|
||||||
|
size=image_size,
|
||||||
|
response_format='url',
|
||||||
|
watermark=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_url = result.data[0].url
|
||||||
|
|
||||||
|
# ── Stage 2: 处理中 ──
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'processing', 'progress': 70,
|
||||||
|
'message': '正在处理图片...',
|
||||||
|
})
|
||||||
|
|
||||||
|
# 下载临时图片并上传到 OSS
|
||||||
|
image_url = _download_and_upload(temp_url)
|
||||||
|
|
||||||
|
# 更新记录
|
||||||
|
badge_image.image_url = image_url
|
||||||
|
badge_image.generation_status = 'completed'
|
||||||
|
badge_image.save(update_fields=['image_url', 'generation_status'])
|
||||||
|
|
||||||
|
# ── Stage 3: 完成 ──
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'done', 'progress': 100,
|
||||||
|
'message': '生成完成!',
|
||||||
|
'image_url': image_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Badge t2i generation failed: {e}')
|
||||||
|
badge_image.generation_status = 'failed'
|
||||||
|
badge_image.save(update_fields=['generation_status'])
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'error', 'progress': 0,
|
||||||
|
'message': f'生成失败: {str(e)}',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def generate_i2i_stream(user, badge_image, image_bytes, prompt='', style=None,
|
||||||
|
strength=0.7, width=1920, height=1920):
|
||||||
|
"""
|
||||||
|
图生图 SSE 流。
|
||||||
|
将用户上传的参考图 + 提示词发给豆包模型,生成正方形新图。
|
||||||
|
"""
|
||||||
|
config = settings.LLM_CONFIG
|
||||||
|
|
||||||
|
if not config.get('API_KEY'):
|
||||||
|
badge_image.generation_status = 'failed'
|
||||||
|
badge_image.save(update_fields=['generation_status'])
|
||||||
|
yield sse_event({'stage': 'error', 'message': 'AI 服务未配置'})
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from volcenginesdkarkruntime import Ark
|
||||||
|
except ImportError:
|
||||||
|
badge_image.generation_status = 'failed'
|
||||||
|
badge_image.save(update_fields=['generation_status'])
|
||||||
|
yield sse_event({'stage': 'error', 'message': 'AI SDK 未安装'})
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Stage 1: 生成中 ──
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'generating', 'progress': 20,
|
||||||
|
'message': '正在根据参考图生成...',
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = Ark(api_key=config['API_KEY'])
|
||||||
|
|
||||||
|
# 先上传参考图到 OSS 获取 URL
|
||||||
|
ref_url = _upload_reference_image(image_bytes)
|
||||||
|
badge_image.reference_image_url = ref_url
|
||||||
|
badge_image.save(update_fields=['reference_image_url'])
|
||||||
|
|
||||||
|
# 构建提示词
|
||||||
|
full_prompt = _build_prompt(prompt or '基于参考图生成类似风格的图片', style)
|
||||||
|
image_model = config.get('IMAGE_MODEL_NAME', 'doubao-seedream-4-5-251128')
|
||||||
|
image_size = f'{width}x{height}'
|
||||||
|
|
||||||
|
# 尝试带参考图的生成,若 SDK 不支持则回退到纯文生图
|
||||||
|
try:
|
||||||
|
result = client.images.generate(
|
||||||
|
model=image_model,
|
||||||
|
prompt=full_prompt,
|
||||||
|
size=image_size,
|
||||||
|
response_format='url',
|
||||||
|
watermark=False,
|
||||||
|
reference_image=ref_url,
|
||||||
|
reference_strength=strength,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
logger.warning('Seedream SDK does not support reference_image, falling back to t2i')
|
||||||
|
result = client.images.generate(
|
||||||
|
model=image_model,
|
||||||
|
prompt=full_prompt,
|
||||||
|
size=image_size,
|
||||||
|
response_format='url',
|
||||||
|
watermark=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_url = result.data[0].url
|
||||||
|
|
||||||
|
# ── Stage 2: 处理中 ──
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'processing', 'progress': 70,
|
||||||
|
'message': '正在处理图片...',
|
||||||
|
})
|
||||||
|
|
||||||
|
image_url = _download_and_upload(temp_url)
|
||||||
|
|
||||||
|
badge_image.image_url = image_url
|
||||||
|
badge_image.generation_status = 'completed'
|
||||||
|
badge_image.save(update_fields=['image_url', 'generation_status'])
|
||||||
|
|
||||||
|
# ── Stage 3: 完成 ──
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'done', 'progress': 100,
|
||||||
|
'message': '生成完成!',
|
||||||
|
'image_url': image_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Badge i2i generation failed: {e}')
|
||||||
|
badge_image.generation_status = 'failed'
|
||||||
|
badge_image.save(update_fields=['generation_status'])
|
||||||
|
yield sse_event({
|
||||||
|
'stage': 'error', 'progress': 0,
|
||||||
|
'message': f'生成失败: {str(e)}',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prompt(prompt, style=None):
|
||||||
|
"""构建完整提示词:用户描述 + 风格后缀 + 正方形构图提示"""
|
||||||
|
parts = [prompt]
|
||||||
|
if style and style in STYLE_PROMPT_MAP:
|
||||||
|
parts.append(STYLE_PROMPT_MAP[style])
|
||||||
|
parts.append('正方形构图,居中主体,适合圆形裁切展示')
|
||||||
|
return ','.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _download_and_upload(temp_url):
|
||||||
|
"""从临时 URL 下载图片,上传到 OSS,返回持久化 URL"""
|
||||||
|
resp = req_lib.get(temp_url, timeout=60)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
from utils.oss import get_oss_client
|
||||||
|
oss_client = get_oss_client()
|
||||||
|
key = f"badge/generated/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.jpg"
|
||||||
|
oss_client.bucket.put_object(
|
||||||
|
key, resp.content,
|
||||||
|
headers={'Content-Type': 'image/jpeg'},
|
||||||
|
)
|
||||||
|
|
||||||
|
oss_config = settings.ALIYUN_OSS
|
||||||
|
if oss_config.get('CUSTOM_DOMAIN'):
|
||||||
|
return f"https://{oss_config['CUSTOM_DOMAIN']}/{key}"
|
||||||
|
return f"https://{oss_config['BUCKET_NAME']}.{oss_config['ENDPOINT']}/{key}"
|
||||||
|
|
||||||
|
|
||||||
|
def _upload_reference_image(image_bytes):
|
||||||
|
"""上传参考图到 OSS,返回 URL"""
|
||||||
|
from utils.oss import get_oss_client
|
||||||
|
oss_client = get_oss_client()
|
||||||
|
key = f"badge/reference/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.jpg"
|
||||||
|
oss_client.bucket.put_object(
|
||||||
|
key, image_bytes,
|
||||||
|
headers={'Content-Type': 'image/jpeg'},
|
||||||
|
)
|
||||||
|
|
||||||
|
oss_config = settings.ALIYUN_OSS
|
||||||
|
if oss_config.get('CUSTOM_DOMAIN'):
|
||||||
|
return f"https://{oss_config['CUSTOM_DOMAIN']}/{key}"
|
||||||
|
return f"https://{oss_config['BUCKET_NAME']}.{oss_config['ENDPOINT']}/{key}"
|
||||||
13
apps/badge/urls.py
Normal file
13
apps/badge/urls.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
电子吧唧模块URL配置
|
||||||
|
"""
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import BadgeViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register('', BadgeViewSet, basename='badge')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('badge/', include(router.urls)),
|
||||||
|
]
|
||||||
108
apps/badge/views.py
Normal file
108
apps/badge/views.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
电子吧唧 - 视图(App端)
|
||||||
|
"""
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
|
||||||
|
from utils.response import success, error
|
||||||
|
from apps.admins.authentication import AppJWTAuthentication
|
||||||
|
from .models import BadgeImage
|
||||||
|
from .serializers import BadgeImageSerializer, Text2ImageSerializer, Image2ImageSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(tags=['电子吧唧'])
|
||||||
|
class BadgeViewSet(viewsets.ViewSet):
|
||||||
|
"""电子吧唧视图集(App端)"""
|
||||||
|
|
||||||
|
authentication_classes = [AppJWTAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='history')
|
||||||
|
def history(self, request):
|
||||||
|
"""
|
||||||
|
获取生成历史
|
||||||
|
GET /api/v1/badge/history/
|
||||||
|
"""
|
||||||
|
images = BadgeImage.objects.filter(user=request.user).order_by('-created_at')[:50]
|
||||||
|
serializer = BadgeImageSerializer(images, many=True)
|
||||||
|
return success(data={'images': serializer.data})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='generate/t2i')
|
||||||
|
def generate_t2i(self, request):
|
||||||
|
"""
|
||||||
|
文生图 (SSE 流式)
|
||||||
|
POST /api/v1/badge/generate/t2i/
|
||||||
|
"""
|
||||||
|
serializer = Text2ImageSerializer(data=request.data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return error(message=str(serializer.errors))
|
||||||
|
|
||||||
|
data = serializer.validated_data
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
badge_image = BadgeImage.objects.create(
|
||||||
|
user=user,
|
||||||
|
prompt=data['prompt'],
|
||||||
|
style=data.get('style', ''),
|
||||||
|
source='t2i',
|
||||||
|
generation_status='generating',
|
||||||
|
)
|
||||||
|
|
||||||
|
from .services.badge_image_service import generate_t2i_stream
|
||||||
|
|
||||||
|
response = StreamingHttpResponse(
|
||||||
|
generate_t2i_stream(
|
||||||
|
user=user,
|
||||||
|
badge_image=badge_image,
|
||||||
|
prompt=data['prompt'],
|
||||||
|
style=data.get('style'),
|
||||||
|
),
|
||||||
|
content_type='text/event-stream',
|
||||||
|
)
|
||||||
|
response['Cache-Control'] = 'no-cache'
|
||||||
|
response['X-Accel-Buffering'] = 'no'
|
||||||
|
return response
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='generate/i2i')
|
||||||
|
def generate_i2i(self, request):
|
||||||
|
"""
|
||||||
|
图生图 (SSE 流式)
|
||||||
|
POST /api/v1/badge/generate/i2i/
|
||||||
|
"""
|
||||||
|
serializer = Image2ImageSerializer(data=request.data, context={'request': request})
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return error(message=str(serializer.errors))
|
||||||
|
|
||||||
|
data = serializer.validated_data
|
||||||
|
user = request.user
|
||||||
|
image_file = data['image']
|
||||||
|
image_bytes = image_file.read()
|
||||||
|
|
||||||
|
badge_image = BadgeImage.objects.create(
|
||||||
|
user=user,
|
||||||
|
prompt=data.get('prompt', ''),
|
||||||
|
style=data.get('style', ''),
|
||||||
|
source='i2i',
|
||||||
|
strength=data.get('strength', 0.7),
|
||||||
|
generation_status='generating',
|
||||||
|
)
|
||||||
|
|
||||||
|
from .services.badge_image_service import generate_i2i_stream
|
||||||
|
|
||||||
|
response = StreamingHttpResponse(
|
||||||
|
generate_i2i_stream(
|
||||||
|
user=user,
|
||||||
|
badge_image=badge_image,
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
prompt=data.get('prompt', ''),
|
||||||
|
style=data.get('style'),
|
||||||
|
strength=data.get('strength', 0.7),
|
||||||
|
),
|
||||||
|
content_type='text/event-stream',
|
||||||
|
)
|
||||||
|
response['Cache-Control'] = 'no-cache'
|
||||||
|
response['X-Accel-Buffering'] = 'no'
|
||||||
|
return response
|
||||||
@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
|||||||
'apps.admins',
|
'apps.admins',
|
||||||
'apps.stories',
|
'apps.stories',
|
||||||
'apps.music',
|
'apps.music',
|
||||||
|
'apps.badge',
|
||||||
'apps.notifications',
|
'apps.notifications',
|
||||||
'apps.system',
|
'apps.system',
|
||||||
]
|
]
|
||||||
@ -220,6 +221,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
{'name': '智能体', 'description': 'AI 智能体 CRUD 和绑定'},
|
{'name': '智能体', 'description': 'AI 智能体 CRUD 和绑定'},
|
||||||
{'name': '故事', 'description': '故事列表、书架管理、生成'},
|
{'name': '故事', 'description': '故事列表、书架管理、生成'},
|
||||||
{'name': '音乐', 'description': '音乐播放列表、收藏、生成'},
|
{'name': '音乐', 'description': '音乐播放列表、收藏、生成'},
|
||||||
|
{'name': '电子吧唧', 'description': '电子吧唧 AI 生图、图片管理'},
|
||||||
{'name': '通知', 'description': '通知列表、已读、删除'},
|
{'name': '通知', 'description': '通知列表、已读、删除'},
|
||||||
{'name': '系统', 'description': '意见反馈、版本检查'},
|
{'name': '系统', 'description': '意见反馈、版本检查'},
|
||||||
{'name': '管理员-认证', 'description': '管理员登录和个人信息'},
|
{'name': '管理员-认证', 'description': '管理员登录和个人信息'},
|
||||||
|
|||||||
@ -21,6 +21,7 @@ app_api_patterns = [
|
|||||||
path('', include('apps.devices.urls')),
|
path('', include('apps.devices.urls')),
|
||||||
path('', include('apps.stories.urls')),
|
path('', include('apps.stories.urls')),
|
||||||
path('', include('apps.music.urls')),
|
path('', include('apps.music.urls')),
|
||||||
|
path('', include('apps.badge.urls')),
|
||||||
path('', include('apps.notifications.urls')),
|
path('', include('apps.notifications.urls')),
|
||||||
path('', include('apps.system.urls')),
|
path('', include('apps.system.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# 故事音频预转码方案 — MP3 → Opus 预处理
|
# 故事音频预转码方案 — MP3 → Opus 预处理
|
||||||
|
|
||||||
> 创建时间:2026-03-03
|
> 创建时间:2026-03-07
|
||||||
> 状态:待实施
|
> 状态:已实施
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,10 @@ class ErrorCode:
|
|||||||
MUSIC_GENERATING = 702
|
MUSIC_GENERATING = 702
|
||||||
MUSIC_DEFAULT_UNDELETABLE = 703
|
MUSIC_DEFAULT_UNDELETABLE = 703
|
||||||
|
|
||||||
|
# 吧唧模块 750-799
|
||||||
|
BADGE_IMAGE_NOT_FOUND = 750
|
||||||
|
BADGE_GENERATE_FAILED = 751
|
||||||
|
|
||||||
# 通知模块 800-899
|
# 通知模块 800-899
|
||||||
NOTIFICATION_NOT_FOUND = 800
|
NOTIFICATION_NOT_FOUND = 800
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user