Showing preview only (1,722K chars total). Download the full file or copy to clipboard to get everything.
Repository: chatfire-AI/huobao-drama
Branch: master
Commit: 43d048179a6e
Files: 183
Total size: 32.4 MB
Directory structure:
gitextract_vm6sptpc/
├── .dockerignore
├── .gitignore
├── DOCKER_HOST_ACCESS.md
├── Dockerfile
├── LICENSE
├── MIGRATE_README.md
├── README-CN.md
├── README-JA.md
├── README.md
├── api/
│ ├── handlers/
│ │ ├── ai_config.go
│ │ ├── asset.go
│ │ ├── audio_extraction.go
│ │ ├── character_batch.go
│ │ ├── character_library.go
│ │ ├── character_library_gen.go
│ │ ├── drama.go
│ │ ├── frame_prompt.go
│ │ ├── frame_prompt_query.go
│ │ ├── image_generation.go
│ │ ├── prop.go
│ │ ├── scene.go
│ │ ├── script_generation.go
│ │ ├── settings.go
│ │ ├── storyboard.go
│ │ ├── task.go
│ │ ├── upload.go
│ │ ├── video_generation.go
│ │ └── video_merge.go
│ ├── middlewares/
│ │ ├── cors.go
│ │ ├── logger.go
│ │ └── ratelimit.go
│ └── routes/
│ └── routes.go
├── application/
│ └── services/
│ ├── ai_service.go
│ ├── asset_duration_update.go
│ ├── asset_service.go
│ ├── audio_extraction_service.go
│ ├── character_library_service.go
│ ├── data_migration_service.go
│ ├── drama_service.go
│ ├── frame_prompt_helper.go
│ ├── frame_prompt_service.go
│ ├── image_generation_service.go
│ ├── prompt_i18n.go
│ ├── prop_service.go
│ ├── resource_transfer_service.go
│ ├── script_generation_service.go
│ ├── storyboard_composition_service.go
│ ├── storyboard_service.go
│ ├── storyboard_update_full.go
│ ├── task_service.go
│ ├── upload_service.go
│ ├── video_generation_service.go
│ └── video_merge_service.go
├── cmd/
│ └── migrate/
│ └── main.go
├── configs/
│ └── config.example.yaml
├── docker-compose.yml
├── docs/
│ └── DATA_MIGRATION.md
├── domain/
│ └── models/
│ ├── ai_config.go
│ ├── asset.go
│ ├── character_library.go
│ ├── drama.go
│ ├── frame_prompt.go
│ ├── image_generation.go
│ ├── task.go
│ ├── timeline.go
│ ├── video_generation.go
│ └── video_merge.go
├── go.mod
├── go.sum
├── infrastructure/
│ ├── database/
│ │ ├── custom_logger.go
│ │ └── database.go
│ ├── external/
│ │ └── ffmpeg/
│ │ └── ffmpeg.go
│ ├── scheduler/
│ │ └── resource_transfer_scheduler.go
│ └── storage/
│ └── local_storage.go
├── main.go
├── migrations/
│ ├── 20260126_add_local_path.sql
│ └── init.sql
├── pkg/
│ ├── ai/
│ │ ├── client.go
│ │ ├── gemini_client.go
│ │ └── openai_client.go
│ ├── config/
│ │ └── config.go
│ ├── image/
│ │ ├── gemini_image_client.go
│ │ ├── image_client.go
│ │ ├── openai_image_client.go
│ │ └── volcengine_image_client.go
│ ├── logger/
│ │ └── logger.go
│ ├── response/
│ │ └── response.go
│ ├── utils/
│ │ ├── image_utils.go
│ │ ├── json_parser.go
│ │ └── json_parser_test.go
│ └── video/
│ ├── chatfire_client.go
│ ├── minimax_client.go
│ ├── openai_sora_client.go
│ ├── video_client.go
│ └── volces_ark_client.go
└── web/
├── .gitignore
├── index.html
├── nginx.conf
├── package.json
├── public/
│ └── ffmpeg/
│ ├── ffmpeg-core.js
│ └── ffmpeg-core.wasm
├── src/
│ ├── App.vue
│ ├── api/
│ │ ├── ai.ts
│ │ ├── asset.ts
│ │ ├── audio.ts
│ │ ├── character-library.ts
│ │ ├── drama.ts
│ │ ├── frame.ts
│ │ ├── generation.ts
│ │ ├── image.ts
│ │ ├── prop.ts
│ │ ├── settings.ts
│ │ ├── task.ts
│ │ ├── video.ts
│ │ └── videoMerge.ts
│ ├── assets/
│ │ └── styles/
│ │ ├── element/
│ │ │ └── index.scss
│ │ └── main.css
│ ├── components/
│ │ ├── LanguageSwitcher.vue
│ │ ├── common/
│ │ │ ├── AIConfigDialog.vue
│ │ │ ├── ActionButton.vue
│ │ │ ├── AppHeader.vue
│ │ │ ├── AppLayout.vue
│ │ │ ├── BaseCard.vue
│ │ │ ├── CreateDramaDialog.vue
│ │ │ ├── EmptyState.vue
│ │ │ ├── ImageCropDialog.vue
│ │ │ ├── ImagePreview.vue
│ │ │ ├── PageHeader.vue
│ │ │ ├── ProjectCard.vue
│ │ │ ├── StatCard.vue
│ │ │ ├── ThemeToggle.vue
│ │ │ └── index.ts
│ │ └── editor/
│ │ ├── GridImageEditor.vue
│ │ ├── StoryboardEditor.vue
│ │ └── VideoTimelineEditor.vue
│ ├── locales/
│ │ ├── en-US.ts
│ │ ├── index.ts
│ │ └── zh-CN.ts
│ ├── main.ts
│ ├── router/
│ │ └── index.ts
│ ├── stores/
│ │ └── episode.ts
│ ├── types/
│ │ ├── ai.ts
│ │ ├── asset.ts
│ │ ├── drama.ts
│ │ ├── generation.ts
│ │ ├── image.ts
│ │ ├── prop.ts
│ │ ├── timeline.ts
│ │ ├── user.ts
│ │ └── video.ts
│ ├── utils/
│ │ ├── ffmpeg.ts
│ │ ├── image.ts
│ │ ├── request.ts
│ │ └── videoMerger.ts
│ └── views/
│ ├── dashboard/
│ │ └── Dashboard.vue
│ ├── drama/
│ │ ├── DramaCreate.vue
│ │ ├── DramaList.vue
│ │ ├── DramaManagement.vue
│ │ ├── DramaWorkflow.vue
│ │ ├── EpisodeWorkflow.vue
│ │ ├── ProfessionalEditor.vue
│ │ └── components/
│ │ └── UploadScriptDialog.vue
│ ├── editor/
│ │ └── TimelineEditor.vue
│ ├── generation/
│ │ ├── ImageGeneration.vue
│ │ ├── VideoGeneration.vue
│ │ └── components/
│ │ ├── GenerateImageDialog.vue
│ │ ├── GenerateVideoDialog.vue
│ │ ├── ImageDetailDialog.vue
│ │ └── VideoDetailDialog.vue
│ ├── script/
│ │ └── ScriptEdit.vue
│ ├── settings/
│ │ ├── AIConfig.vue
│ │ ├── SystemSettings.vue
│ │ └── components/
│ │ └── ConfigList.vue
│ ├── storyboard/
│ │ └── StoryboardEdit.vue
│ └── workflow/
│ ├── CharacterExtraction.vue
│ ├── CharacterImages.vue
│ ├── DramaSettings.vue
│ ├── SceneImages.vue
│ └── StoryboardGeneration.vue
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Git
.git
.gitignore
.gitattributes
# IDE
.idea
.vscode
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# 构建产物
dist/
build/
*.exe
*.dll
*.so
*.dylib
huobao-drama
backend
# 依赖
node_modules/
**/node_modules/
vendor/
# 数据文件
data/
*.db
*.log
# 临时文件
tmp/
temp/
# 前端构建缓存
web/.vite/
web/dist/
# 测试
*.test
coverage/
# 文档
README.md
drama.png
*.md
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore
# CI/CD
.github/
.gitlab-ci.yml
================================================
FILE: .gitignore
================================================
# Binaries
bin/
drama-generator
backend
*.exe
*.dll
*.so
*.dylib
drama-generator.exe
# Test binary
*.test
# Output of the go coverage tool
*.out
# Dependency directories
vendor/
# Go workspace file
go.work
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Environment
.env
.env.local
# Logs
*.log
# OS
.DS_Store
Thumbs.db
# Build
dist/
build/
# Temporary files
tmp/
temp/
# Data (database and uploaded files)
data/drama_generator.db
data/storage/videos/*
!data/storage/videos/.gitkeep
# Frontend build output
web/dist/
web/node_modules/
web/.vite/
web/.env.local
# Config file (use config.example.yaml as template)
configs/config.yaml
# Docker publish documentation (optional)
DOCKER_PUBLISH.md
build.sh
/data/storage/
/web/package-lock.json
.DS_Store
================================================
FILE: DOCKER_HOST_ACCESS.md
================================================
# Docker 容器访问宿主机服务指南
## 核心配置
Docker 容器内使用 `http://host.docker.internal:端口号` 访问宿主机服务。
### macOS / Windows
直接使用,无需额外配置。
### Linux
**docker-compose** - 已在 `docker-compose.yml` 配置:
```yaml
extra_hosts:
- "host.docker.internal:host-gateway"
```
**docker run** - 需添加参数:
```bash
docker run --add-host=host.docker.internal:host-gateway ...
```
## Ollama 配置示例
### 1. 宿主机启动服务
```bash
# 监听所有接口(重要)
export OLLAMA_HOST=0.0.0.0:11434
ollama serve
```
### 2. 前端 AI 服务配置
| 字段 | 值 |
|------|-----|
| Base URL | `http://host.docker.internal:11434/v1` |
| Provider | `openai` |
| Model | `qwen2.5:latest` |
| API Key | `ollama` 或留空 |
### 3. 其他服务端口
| 服务 | 默认端口 | Base URL |
|------|---------|----------|
| Ollama | 11434 | `http://host.docker.internal:11434/v1` |
| LM Studio | 1234 | `http://host.docker.internal:1234/v1` |
| vLLM | 8000 | `http://host.docker.internal:8000/v1` |
## 验证和故障排查
### 测试连接
```bash
# 进入容器测试
docker exec -it huobao-drama sh
wget -O- http://host.docker.internal:11434/api/tags
# 查看容器日志
docker logs huobao-drama -f
```
### 常见问题
**Connection refused**
1. **宿主机服务未运行** - 检查服务状态
```bash
curl http://localhost:11434/api/tags
```
2. **服务未监听 0.0.0.0** - Ollama 默认只监听 127.0.0.1
```bash
export OLLAMA_HOST=0.0.0.0:11434
ollama serve
```
3. **防火墙阻止** - 检查防火墙规则或临时关闭测试
================================================
FILE: Dockerfile
================================================
# 多阶段构建 Dockerfile for Huobao Drama
# ==================== 阶段1: 构建前端 ====================
# 声明构建参数(支持镜像源配置)
ARG DOCKER_REGISTRY=
ARG NPM_REGISTRY=
FROM ${DOCKER_REGISTRY:-}node:20-alpine AS frontend-builder
# 重新声明 ARG(FROM 之后 ARG 作用域失效,需要重新声明)
ARG NPM_REGISTRY=
# 配置 npm 镜像源(条件执行)
ENV NPM_REGISTRY=${NPM_REGISTRY:-}
RUN if [ -n "$NPM_REGISTRY" ]; then \
npm config set registry "$NPM_REGISTRY" || true; \
fi
WORKDIR /app/web
# 复制前端依赖文件
COPY web/package*.json ./
# 安装前端依赖(包括 devDependencies,构建需要)
RUN npm install
# 复制前端源码
COPY web/ ./
# 构建前端
RUN npm run build
# ==================== 阶段2: 构建后端 ====================
# 每个阶段前重新声明构建参数
ARG DOCKER_REGISTRY=
ARG GO_PROXY=
ARG ALPINE_MIRROR=
FROM ${DOCKER_REGISTRY:-}golang:1.23-alpine AS backend-builder
# 重新声明 ARG(FROM 之后 ARG 作用域失效,需要重新声明)
ARG GO_PROXY=
ARG ALPINE_MIRROR=
# 配置 Alpine 镜像源(条件执行)
ENV ALPINE_MIRROR=${ALPINE_MIRROR:-}
RUN if [ -n "$ALPINE_MIRROR" ]; then \
sed -i "s@dl-cdn.alpinelinux.org@$ALPINE_MIRROR@g" /etc/apk/repositories 2>/dev/null || true; \
fi
# 配置 Go 代理(使用 ENV 持久化到运行时)
ENV GOPROXY=${GO_PROXY:-https://goproxy.cn,direct}
ENV GO111MODULE=on
# 安装必要的构建工具(纯 Go 编译,无需 CGO)
RUN apk add --no-cache \
git \
ca-certificates \
tzdata
WORKDIR /app
# 复制 Go 模块文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制后端源码
COPY . .
# 复制前端构建产物
COPY --from=frontend-builder /app/web/dist ./web/dist
# 构建后端可执行文件(纯 Go 编译,使用 modernc.org/sqlite)
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o huobao-drama .
# 构建迁移脚本可执行文件
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o migrate cmd/migrate/main.go
# ==================== 阶段3: 运行时镜像 ====================
# 每个阶段前重新声明构建参数
ARG DOCKER_REGISTRY=
ARG ALPINE_MIRROR=
FROM ${DOCKER_REGISTRY:-}alpine:latest
# 重新声明 ARG(FROM 之后 ARG 作用域失效,需要重新声明)
ARG ALPINE_MIRROR=
# 配置 Alpine 镜像源(条件执行)
ENV ALPINE_MIRROR=${ALPINE_MIRROR:-}
RUN if [ -n "$ALPINE_MIRROR" ]; then \
sed -i "s@dl-cdn.alpinelinux.org@$ALPINE_MIRROR@g" /etc/apk/repositories 2>/dev/null || true; \
fi
# 安装运行时依赖
RUN apk add --no-cache \
ca-certificates \
tzdata \
ffmpeg \
wget \
&& rm -rf /var/cache/apk/*
# 设置时区
ENV TZ=Asia/Shanghai
WORKDIR /app
# 从构建阶段复制可执行文件
COPY --from=backend-builder /app/huobao-drama .
COPY --from=backend-builder /app/migrate .
# 复制前端构建产物
COPY --from=frontend-builder /app/web/dist ./web/dist
# 复制配置文件模板并创建默认配置
COPY configs/config.example.yaml ./configs/
RUN cp ./configs/config.example.yaml ./configs/config.yaml
# 复制数据库迁移文件
COPY migrations ./migrations/
# 创建数据目录(root 用户运行,无需权限设置)
RUN mkdir -p /app/data/storage
# 暴露端口
EXPOSE 5678
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:5678/health || exit 1
# 启动应用
CMD ["./huobao-drama"]
================================================
FILE: LICENSE
================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License
Copyright (c) 2026 火宝 (Chatfire)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
要查看该许可协议,可访问:
To view a copy of this license, visit:
https://creativecommons.org/licenses/by-nc-sa/4.0/
或者写信到:
Or send a letter to:
Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
---
个人使用许可 (Personal Use License)
您可以自由地:
You are free to:
- 分享 — 在任何媒介以任何形式复制、发行本作品
Share — copy and redistribute the material in any medium or format
- 演绎 — 修改、转换或以本作品为基础进行创作
Adapt — remix, transform, and build upon the material
惟须遵守下列条件:
Under the following terms:
- 署名 — 您必须给出适当的署名,提供指向本许可协议的链接
Attribution — You must give appropriate credit and provide a link to the license
- 非商业性使用 — 您不得将本作品用于商业目的
NonCommercial — You may not use the material for commercial purposes
- 相同方式共享 — 如果您再混合、转换或者基于本作品进行创作,您必须基于与原先许可协议相同的许可协议分发您贡献的作品
ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license
---
商业授权 (Commercial License)
如需将本项目用于商业目的,请联系作者获取商业授权:
For commercial use, please contact the author for a commercial license:
Email: 18550175439@163.com
WeChat: dangbao1117
GitHub: https://github.com/chatfire-AI
---
免责声明 (Disclaimer)
本软件按"原样"提供,不提供任何形式的明示或暗示担保。
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
================================================
FILE: MIGRATE_README.md
================================================
# 数据清洗工具使用说明
## 使用方法
### 本地部署
```bash
# 在项目根目录执行
go run cmd/migrate/main.go
```
### Docker 部署
在 Docker 容器中,迁移脚本已经被编译为可执行文件 `migrate`。
```bash
# 进入容器
docker exec -it huobao-drama sh
# 在容器内执行迁移脚本
./migrate
# 执行完成后,退出容器
exit
```
或者直接执行(不进入容器):
```bash
docker exec huobao-drama ./migrate
```
## 配置要求
脚本会自动读取项目配置文件,确保以下配置正确:
- 数据库连接信息(`config/config.yaml` 或环境变量)
- 存储目录:`data/storage`(自动创建)
## 输出示例
```
=== 数据清洗工具:迁移 local_path ===
开始时间: 2026-01-27 14:30:00
INFO 初始化日志系统...
INFO 配置加载成功
INFO 数据库连接成功
INFO 开始数据清洗:迁移 local_path 为空的数据
INFO 存储目录创建成功 root=data/storage
INFO 开始迁移 assets 数据...
INFO 找到需要迁移的 assets 数量=5
INFO 处理 asset id=1 name=背景图 type=image url=https://...
INFO 开始下载文件 url=https://... filepath=data/storage/images/asset_1_1738048200.jpg
INFO 文件下载成功 filepath=data/storage/images/asset_1_1738048200.jpg size=245678
INFO 已缓存 URL 映射 url=https://... local_path=images/asset_1_1738048200.jpg
INFO asset 迁移成功 asset_id=1 local_path=images/asset_1_1738048200.jpg
INFO 开始迁移 character_libraries 数据...
INFO 找到需要迁移的 character_libraries 数量=3
INFO 使用缓存的本地路径 url=https://... local_path=characters/charlib_2_1738048201.jpg
INFO 开始迁移 image_generations 数据...
INFO 找到需要迁移的 image_generations 数量=10
INFO 处理 image_generation id=15 image_type=character image_url=https://...
INFO image_generation 迁移成功 imggen_id=15 local_path=characters/imggen_15_1738048205.jpg
INFO 数据清洗完成
总耗时=25.5s
URL映射缓存数=8
Assets成功=5 Assets失败=0
角色库成功=3 角色库失败=0
角色成功=4 角色失败=0
图片生成成功=10 图片生成失败=0
场景成功=6 场景失败=0
视频成功=2 视频失败=0
=== 数据清洗完成 ===
结束时间: 2026-01-27 14:30:25
```
## 注意事项
1. **运行前确保**:
- 数据库可访问
- 有足够的磁盘空间
- 网络连接正常
2. **安全提示**:
- 脚本会修改数据库中的 `local_path` 字段
- 建议先在测试环境运行
- 可以多次运行,已处理的数据会自动跳过
3. **性能优化**:
- URL 缓存机制避免重复下载
- 下载失败会跳过,不影响其他数据
- 超时时间设置为 60 秒
## 常见问题
### Q: 脚本可以重复运行吗?
A: 可以。脚本只处理 `local_path` 为空的记录,已处理的数据会自动跳过。
### Q: 下载失败怎么办?
A: 单个文件下载失败会记录错误日志并继续处理其他文件。可以查看日志定位问题后重新运行。
### Q: 如何查看详细日志?
A: 日志会实时输出到控制台,包含每个文件的处理状态和最终统计信息。
### Q: 存储路径可以修改吗?
A: 可以。修改脚本中的 `storageRoot` 变量(默认为 `data/storage`)。
## 技术支持
如有问题,请查看日志输出或联系开发团队。
================================================
FILE: README-CN.md
================================================
# 🎬 Huobao Drama - AI 短剧生成平台
<div align="center">
**基于 Go + Vue3 的全栈 AI 短剧自动化生产平台**
[](https://golang.org)
[](https://vuejs.org)
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
[功能特性](#功能特性) • [快速开始](#快速开始) • [部署指南](#部署指南)
[简体中文](README-CN.md) | [English](README.md) | [日本語](README-JA.md)
</div>
---
## 📖 项目简介
Huobao Drama 是一个基于 AI 的短剧自动化生产平台,实现从剧本生成、角色设计、分镜制作到视频合成的全流程自动化。
火宝短剧商业版地址:[火宝短剧商业版](https://drama.chatfire.site/shortvideo)
火宝小说生成:[火宝小说生成](https://marketing.chatfire.site/huobao-novel/)
### 🎯 核心价值
- **🤖 AI 驱动**:使用大语言模型解析剧本,提取角色、场景和分镜信息
- **🎨 智能创作**:AI 绘图生成角色形象和场景背景
- **📹 视频生成**:基于文生视频和图生视频模型自动生成分镜视频
- **🔄 工作流**:完整的短剧制作工作流,从创意到成片一站式完成
### 🛠️ 技术架构
采用**DDD 领域驱动设计**,清晰分层:
```
├── API层 (Gin HTTP)
├── 应用服务层 (Business Logic)
├── 领域层 (Domain Models)
└── 基础设施层 (Database, External Services)
```
### 🎥 作品展示 / Demo Videos
体验 AI 短剧生成效果:
<div align="center">
**示例作品 1**
<video src="https://ffile.chatfire.site/cf/public/20260114094337396.mp4" controls width="640"></video>
**示例作品 2**
<video src="https://ffile.chatfire.site/cf/public/fcede75e8aeafe22031dbf78f86285b8.mp4" controls width="640"></video>
[点击观看视频 1](https://ffile.chatfire.site/cf/public/20260114094337396.mp4) | [点击观看视频 2](https://ffile.chatfire.site/cf/public/fcede75e8aeafe22031dbf78f86285b8.mp4)
</div>
---
## ✨ 功能特性
### 🎭 角色管理
- ✅ AI 生成角色形象
- ✅ 批量角色生成
- ✅ 角色图片上传和管理
### 🎬 分镜制作
- ✅ 自动生成分镜脚本
- ✅ 场景描述和镜头设计
- ✅ 分镜图片生成(文生图)
- ✅ 帧类型选择(首帧/关键帧/尾帧/分镜板)
### 🎥 视频生成
- ✅ 图生视频自动生成
- ✅ 视频合成和剪辑
- ✅ 转场效果
### 📦 资源管理
- ✅ 素材库统一管理
- ✅ 本地存储支持
- ✅ 资源导入导出
- ✅ 任务进度追踪
---
## 🚀 快速开始
### 📋 环境要求
| 软件 | 版本要求 | 说明 |
| ----------- | -------- | -------------------- |
| **Go** | 1.23+ | 后端运行环境 |
| **Node.js** | 18+ | 前端构建环境 |
| **npm** | 9+ | 包管理工具 |
| **FFmpeg** | 4.0+ | 视频处理(**必需**) |
| **SQLite** | 3.x | 数据库(已内置) |
#### 安装 FFmpeg
**macOS:**
```bash
brew install ffmpeg
```
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install ffmpeg
```
**Windows:**
从 [FFmpeg 官网](https://ffmpeg.org/download.html) 下载并配置环境变量
验证安装:
```bash
ffmpeg -version
```
### ⚙️ 配置文件
复制并编辑配置文件:
```bash
cp configs/config.example.yaml configs/config.yaml
vim configs/config.yaml
```
配置文件格式(`configs/config.yaml`):
```yaml
app:
name: "Huobao Drama API"
version: "1.0.0"
debug: true # 开发环境设为true,生产环境设为false
server:
port: 5678
host: "0.0.0.0"
cors_origins:
- "http://localhost:3012"
read_timeout: 600
write_timeout: 600
database:
type: "sqlite"
path: "./data/drama_generator.db"
max_idle: 10
max_open: 100
storage:
type: "local"
local_path: "./data/storage"
base_url: "http://localhost:5678/static"
ai:
default_text_provider: "openai"
default_image_provider: "openai"
default_video_provider: "doubao"
```
**重要配置项:**
- `app.debug`: 调试模式开关(开发环境建议设为 true)
- `server.port`: 服务运行端口
- `server.cors_origins`: 允许跨域访问的前端地址
- `database.path`: SQLite 数据库文件路径
- `storage.local_path`: 本地文件存储路径
- `storage.base_url`: 静态资源访问 URL
- `ai.default_*_provider`: AI 服务提供商配置(在 Web 界面中配置具体的 API Key)
### 📥 安装依赖
```bash
# 克隆项目
git clone https://github.com/chatfire-AI/huobao-drama.git
cd huobao-drama
# 安装Go依赖
go mod download
# 安装前端依赖
cd web
npm install
cd ..
```
### 🎯 启动项目
#### 方式一:开发模式(推荐)
**前后端分离,支持热重载**
```bash
# 终端1:启动后端服务
go run main.go
# 终端2:启动前端开发服务器
cd web
npm run dev
```
- 前端地址: `http://localhost:3012`
- 后端 API: `http://localhost:5678/api/v1`
- 前端自动代理 API 请求到后端
#### 方式二:单服务模式
**后端同时提供 API 和前端静态文件**
```bash
# 1. 构建前端
cd web
npm run build
cd ..
# 2. 启动服务
go run main.go
```
访问: `http://localhost:5678`
### 🗄️ 数据库初始化
数据库表会在首次启动时自动创建(使用 GORM AutoMigrate),无需手动迁移。
---
## 📦 部署指南
### ☁️ 云端一键部署(推荐 3080Ti)
👉 [优云智算,一键部署](https://www.compshare.cn/images/fScvzK95NUk5?referral_code=8hUJOaWz3YzG64FI2OlCiB&ytag=GPU_YY_YX_GitHub_huobaoai)
> ⚠️ **注意**:云端部署方案数据请及时存储到本地
---
### 🐳 Docker 部署(推荐)
#### 方式一:Docker Compose(推荐)
#### 🚀 国内网络加速(可选)
如果您在国内网络环境下,Docker 拉取镜像和安装依赖可能较慢。可以通过配置镜像源加速构建过程。
**步骤 1:创建环境变量文件**
```bash
cp .env.example .env
```
**步骤 2:编辑 `.env` 文件,取消注释需要的镜像源**
```bash
# 启用 Docker Hub 镜像(推荐)
DOCKER_REGISTRY=docker.1ms.run/
# 启用 npm 镜像
NPM_REGISTRY=https://registry.npmmirror.com/
# 启用 Go 代理
GO_PROXY=https://goproxy.cn,direct
# 启用 Alpine 镜像
ALPINE_MIRROR=mirrors.aliyun.com
```
**步骤 3:使用 docker compose 构建(必须)**
```bash
docker compose build
```
> **重要说明**:
>
> - ⚠️ 必须使用 `docker compose build` 才能自动加载 `.env` 文件中的镜像源配置
> - ❌ 如果使用 `docker build` 命令,需要手动传递 `--build-arg` 参数
> - ✅ 推荐始终使用 `docker compose build` 进行构建
**效果对比**:
| 操作 | 不配置镜像源 | 配置镜像源后 |
| ------------- | ------------ | ------------ |
| 拉取基础镜像 | 5-30 分钟 | 1-5 分钟 |
| 安装 npm 依赖 | 可能失败 | 快速成功 |
| 下载 Go 依赖 | 5-10 分钟 | 30 秒-1 分钟 |
> **注意**:国外用户请勿配置镜像源,使用默认配置即可。
```bash
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
#### 方式二:Docker 命令
> **注意**:Linux 用户需添加 `--add-host=host.docker.internal:host-gateway` 以访问宿主机服务
```bash
# 从 Docker Hub 运行
docker run -d \
--name huobao-drama \
-p 5678:5678 \
-v $(pwd)/data:/app/data \
--restart unless-stopped \
huobao/huobao-drama:latest
# 查看日志
docker logs -f huobao-drama
```
**本地构建**(可选):
```bash
docker build -t huobao-drama:latest .
docker run -d --name huobao-drama -p 5678:5678 -v $(pwd)/data:/app/data huobao-drama:latest
```
**Docker 部署优势:**
- ✅ 开箱即用,内置默认配置
- ✅ 环境一致性,避免依赖问题
- ✅ 一键启动,无需安装 Go、Node.js、FFmpeg
- ✅ 易于迁移和扩展
- ✅ 自动健康检查和重启
- ✅ 自动处理文件权限,无需手动配置
#### 🔗 访问宿主机服务(Ollama/本地模型)
容器已配置支持访问宿主机服务,直接使用 `http://host.docker.internal:端口号` 即可。
**配置步骤:**
1. **宿主机启动服务(监听所有接口)**
```bash
export OLLAMA_HOST=0.0.0.0:11434 && ollama serve
```
2. **前端 AI 服务配置**
- Base URL: `http://host.docker.internal:11434/v1`
- Provider: `openai`
- Model: `qwen2.5:latest`
---
### 🏭 传统部署方式
#### 1. 编译构建
```bash
# 1. 构建前端
cd web
npm run build
cd ..
# 2. 编译后端
go build -o huobao-drama .
```
生成文件:
- `huobao-drama` - 后端可执行文件
- `web/dist/` - 前端静态文件(已嵌入后端)
#### 2. 准备部署文件
需要上传到服务器的文件:
```
huobao-drama # 后端可执行文件
configs/config.yaml # 配置文件
data/ # 数据目录(可选,首次运行自动创建)
```
#### 3. 服务器配置
```bash
# 上传文件到服务器
scp huobao-drama user@server:/opt/huobao-drama/
scp configs/config.yaml user@server:/opt/huobao-drama/configs/
# SSH登录服务器
ssh user@server
# 修改配置文件
cd /opt/huobao-drama
vim configs/config.yaml
# 设置mode为production
# 配置域名和存储路径
# 创建数据目录并设置权限(重要!)
# 注意:将 YOUR_USER 替换为实际运行服务的用户名(如 www-data、ubuntu、deploy 等)
sudo mkdir -p /opt/huobao-drama/data/storage
sudo chown -R YOUR_USER:YOUR_USER /opt/huobao-drama/data
sudo chmod -R 755 /opt/huobao-drama/data
# 赋予执行权限
chmod +x huobao-drama
# 启动服务
./huobao-drama
```
#### 4. 使用 systemd 管理服务
创建服务文件 `/etc/systemd/system/huobao-drama.service`:
```ini
[Unit]
Description=Huobao Drama Service
After=network.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/opt/huobao-drama
ExecStart=/opt/huobao-drama/huobao-drama
Restart=on-failure
RestartSec=10
# 环境变量(可选)
# Environment="GIN_MODE=release"
[Install]
WantedBy=multi-user.target
```
启动服务:
```bash
sudo systemctl daemon-reload
sudo systemctl enable huobao-drama
sudo systemctl start huobao-drama
sudo systemctl status huobao-drama
```
**⚠️ 常见问题:SQLite 写权限错误**
如果遇到 `attempt to write a readonly database` 错误:
```bash
# 1. 确认当前运行服务的用户
sudo systemctl status huobao-drama | grep "Main PID"
ps aux | grep huobao-drama
# 2. 修复权限(将 YOUR_USER 替换为实际用户名)
sudo chown -R YOUR_USER:YOUR_USER /opt/huobao-drama/data
sudo chmod -R 755 /opt/huobao-drama/data
# 3. 验证权限
ls -la /opt/huobao-drama/data
# 应该显示所有者为运行服务的用户
# 4. 重启服务
sudo systemctl restart huobao-drama
```
**原因说明**:
- SQLite 需要对数据库文件 **和** 所在目录都有写权限
- 需要在目录中创建临时文件(如 `-wal`、`-journal`)
- **关键**:确保 systemd 配置中的 `User` 与数据目录所有者一致
**常用用户名**:
- Ubuntu/Debian: `www-data`、`ubuntu`
- CentOS/RHEL: `nginx`、`apache`
- 自定义部署: `deploy`、`app`、当前登录用户
#### 5. Nginx 反向代理
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 静态文件直接访问
location /static/ {
alias /opt/huobao-drama/data/storage/;
}
}
```
---
## 🎨 技术栈
### 后端技术
- **语言**: Go 1.23+
- **Web 框架**: Gin 1.9+
- **ORM**: GORM
- **数据库**: SQLite
- **日志**: Zap
- **视频处理**: FFmpeg
- **AI 服务**: OpenAI、Gemini、火山等
### 前端技术
- **框架**: Vue 3.4+
- **语言**: TypeScript 5+
- **构建工具**: Vite 5
- **UI 组件**: Element Plus
- **CSS 框架**: TailwindCSS
- **状态管理**: Pinia
- **路由**: Vue Router 4
### 开发工具
- **包管理**: Go Modules, npm
- **代码规范**: ESLint, Prettier
- **版本控制**: Git
---
## 📝 常见问题
### Q: Docker 容器如何访问宿主机的 Ollama?
A: 使用 `http://host.docker.internal:11434/v1` 作为 Base URL。注意两点:
1. 宿主机 Ollama 需监听 `0.0.0.0`:`export OLLAMA_HOST=0.0.0.0:11434 && ollama serve`
2. Linux 用户使用 `docker run` 需添加:`--add-host=host.docker.internal:host-gateway`
详见:[DOCKER_HOST_ACCESS.md](docs/DOCKER_HOST_ACCESS.md)
### Q: FFmpeg 未安装或找不到?
A: 确保 FFmpeg 已安装并在 PATH 环境变量中。运行 `ffmpeg -version` 验证。
### Q: 前端无法连接后端 API?
A: 检查后端是否启动,端口是否正确。开发模式下前端代理配置在 `web/vite.config.ts`。
### Q: 数据库表未创建?
A: GORM 会在首次启动时自动创建表,检查日志确认迁移是否成功。
---
## 📋 更新日志 / Changelog
### v1.0.5 (2026-02-06)
#### 🎨 重大功能
- **🎭 全局风格系统**:引入了项目级别的风格选择支持。用户现在可以在剧本层面定义自定义视觉风格,该风格将自动应用于所有 AI 生成的内容,包括角色、场景和分镜图像,确保整个制作过程中的艺术风格一致性。
- **✂️ 九宫格序列图裁剪**:新增裁剪工具,支持从动作序列图(3x3 网格布局)中提取单个帧,并将其指定为首帧、尾帧或关键帧用于视频生成,为镜头构图和连续性提供更大的灵活性。
#### 🚀 功能增强
- **📐 优化动作序列网格**:改进了九宫格动作序列图的视觉质量和布局,优化了间距、对齐和帧过渡效果。
- **🔧 手动网格拼接**:引入手动网格组合工具,支持 2x2(四宫格)、2x3(六宫格)和 3x3(九宫格)布局,允许用户从单个帧创建自定义动作序列。
- **🗑️ 内容管理**:新增图片和视频的删除功能,实现更好的素材组织和存储管理。
### v1.0.4 (2026-01-27)
#### 🚀 重大更新
- 引入本地存储策略,实现生成内容的本地化缓存管理,有效规避外部资源链接失效风险
- 采用 Base64 编码方案进行参考图像的嵌入式传输
- 修复镜头切换时镜头图片提示词状态未重置问题
- 修复视频添加素材库视频时长显示为0的问题
- 添加场景迁移至章节内
#### 历史数据清洗
- 增加清洗脚本,用于处理历史数据,具体操作请参考 [MIGRATE_README.md](MIGRATE_README.md)
### v1.0.3 (2026-01-16)
#### 🚀 重大更新
- SQLite 纯 Go 驱动(`modernc.org/sqlite`),支持 `CGO_ENABLED=0` 跨平台编译
- 优化并发性能(WAL 模式),解决 "database is locked" 错误
- Docker 跨平台支持 `host.docker.internal` 访问宿主机服务
- 精简文档和部署指南
### v1.0.2 (2026-01-14)
#### 🐛 Bug Fixes / 🔧 Improvements
- 修复视频生成 API 响应解析问题
- 添加 OpenAI Sora 视频端点配置
- 优化错误处理和日志输出
---
## 🤝 贡献指南
欢迎提交 Issue 和 Pull Request!
1. Fork 本项目
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交改动 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request
---
## API 配置站点
2 分钟完成配置:[API 聚合站点](https://api.chatfire.site/models)
---
## 👨💻 关于我们
**AI 火宝 - AI 工作室创业中**
- 🏠 **位置**: 中国南京
- 🚀 **状态**: 创业中
- 📧 **Email**: [18550175439@163.com](mailto:18550175439@163.com)
- 🐙 **GitHub**: [https://github.com/chatfire-AI/huobao-drama](https://github.com/chatfire-AI/huobao-drama)
> _"让 AI 帮我们做更有创造力的事"_
### 📢 招聘信息
#### 全栈高级开发工程师(Base 南京)
**关于岗位:**
我们是创业团队,致力于打造 AI 驱动的终端应用。如果你热爱技术、追求极致,渴望在 AI 领域有所作为,欢迎加入我们,一起做有意义的事!
**岗位要求:**
1. 参与 AI 驱动的终端应用全栈开发,覆盖 Web/移动端前后端系统搭建,主导核心业务模块的设计、开发、测试与上线迭代。
2. 负责核心功能模块开发,包括:前端交互实现(React/Vue.js + TypeScript)、后端服务开发(Python/Node.js + Flask/FastAPI),确保模块高效、稳定、可扩展。
3. 负责 AI 模型服务接口设计与对接,参与 AI 能力落地终端场景的技术方案设计,推动 AI 技术与业务场景的深度融合。
4. 深度践行 vibe coding 开发模式,注重代码质量、开发效率与技术美感,搭建规范的开发流程,推动团队研发模式优化。
**加入我们:**
如果你对 AI 技术充满热情,喜欢挑战和创新,欢迎投递简历至 [18550175439@163.com](mailto:18550175439@163.com)
期待志同道合的你一起探索 AI 的无限可能!
## 项目交流群

- 提交 [Issue](../../issues)
- 发送邮件至项目维护者
---
<div align="center">
**⭐ 如果这个项目对你有帮助,请给一个 Star!**
## Star History
[](https://www.star-history.com/#chatfire-AI/huobao-drama&type=date&legend=top-left)
Made with ❤️ by Huobao Team
</div>
================================================
FILE: README-JA.md
================================================
# 🎬 Huobao Drama - AI ショートドラマ制作プラットフォーム
<div align="center">
**Go + Vue3 ベースのフルスタック AI ショートドラマ自動化プラットフォーム**
[](https://golang.org)
[](https://vuejs.org)
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
[機能](#機能) • [クイックスタート](#クイックスタート) • [デプロイ](#デプロイ)
[简体中文](README-CN.md) | [English](README.md) | [日本語](README-JA.md)
</div>
---
## 📖 概要
Huobao Drama は、脚本生成、キャラクターデザイン、絵コンテ作成から動画合成までの全ワークフローを自動化する AI 駆動のショートドラマ制作プラットフォームです。
火宝短剧商业版地址:[火宝短剧商业版](https://drama.chatfire.site/shortvideo)
火宝小说生成:[火宝小说生成](https://marketing.chatfire.site/huobao-novel/)
### 🎯 主要機能
- **🤖 AI 駆動**: 大規模言語モデルを使用して脚本を解析し、キャラクター、シーン、絵コンテ情報を抽出
- **🎨 インテリジェント創作**: AI によるキャラクターポートレートとシーン背景の生成
- **📹 動画生成**: テキストから動画、画像から動画モデルによる絵コンテ動画の自動生成
- **🔄 完全なワークフロー**: アイデアから完成動画までのエンドツーエンド制作ワークフロー
### 🛠️ 技術アーキテクチャ
**DDD(ドメイン駆動設計)** に基づく明確なレイヤー構造:
```
├── APIレイヤー (Gin HTTP)
├── アプリケーションサービスレイヤー (ビジネスロジック)
├── ドメインレイヤー (ドメインモデル)
└── インフラストラクチャレイヤー (データベース、外部サービス)
```
### 🎥 デモ動画
AI ショートドラマ生成を体験:
<div align="center">
**サンプル作品 1**
<video src="https://ffile.chatfire.site/cf/public/20260114094337396.mp4" controls width="640"></video>
**サンプル作品 2**
<video src="https://ffile.chatfire.site/cf/public/fcede75e8aeafe22031dbf78f86285b8.mp4" controls width="640"></video>
[動画 1 を見る](https://ffile.chatfire.site/cf/public/20260114094337396.mp4) | [動画 2 を見る](https://ffile.chatfire.site/cf/public/fcede75e8aeafe22031dbf78f86285b8.mp4)
</div>
---
## ✨ 機能
### 🎭 キャラクター管理
- ✅ AI 生成キャラクターポートレート
- ✅ バッチキャラクター生成
- ✅ キャラクター画像のアップロードと管理
### 🎬 絵コンテ制作
- ✅ 自動絵コンテスクリプト生成
- ✅ シーン説明とショットデザイン
- ✅ 絵コンテ画像生成(テキストから画像)
- ✅ フレームタイプ選択(先頭フレーム/キーフレーム/末尾フレーム/パネル)
### 🎥 動画生成
- ✅ 画像から動画の自動生成
- ✅ 動画合成と編集
- ✅ トランジション効果
### 📦 アセット管理
- ✅ 統合アセットライブラリ管理
- ✅ ローカルストレージサポート
- ✅ アセットのインポート/エクスポート
- ✅ タスク進捗トラッキング
---
## 🚀 クイックスタート
### 📋 前提条件
| ソフトウェア | バージョン | 説明 |
| ------------ | ---------- | ------------------------ |
| **Go** | 1.23+ | バックエンドランタイム |
| **Node.js** | 18+ | フロントエンドビルド環境 |
| **npm** | 9+ | パッケージマネージャー |
| **FFmpeg** | 4.0+ | 動画処理(**必須**) |
| **SQLite** | 3.x | データベース(内蔵) |
#### FFmpeg のインストール
**macOS:**
```bash
brew install ffmpeg
```
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install ffmpeg
```
**Windows:**
[FFmpeg 公式サイト](https://ffmpeg.org/download.html)からダウンロードし、環境変数を設定
インストール確認:
```bash
ffmpeg -version
```
### ⚙️ 設定
設定ファイルをコピーして編集:
```bash
cp configs/config.example.yaml configs/config.yaml
vim configs/config.yaml
```
設定ファイル形式(`configs/config.yaml`):
```yaml
app:
name: "Huobao Drama API"
version: "1.0.0"
debug: true # 開発環境ではtrue、本番環境ではfalseに設定
server:
port: 5678
host: "0.0.0.0"
cors_origins:
- "http://localhost:3012"
read_timeout: 600
write_timeout: 600
database:
type: "sqlite"
path: "./data/drama_generator.db"
max_idle: 10
max_open: 100
storage:
type: "local"
local_path: "./data/storage"
base_url: "http://localhost:5678/static"
ai:
default_text_provider: "openai"
default_image_provider: "openai"
default_video_provider: "doubao"
```
**主要設定項目:**
- `app.debug`: デバッグモードスイッチ(開発環境では true を推奨)
- `server.port`: サービスポート
- `server.cors_origins`: フロントエンドの許可 CORS オリジン
- `database.path`: SQLite データベースファイルパス
- `storage.local_path`: ローカルファイルストレージパス
- `storage.base_url`: 静的リソースアクセス URL
- `ai.default_*_provider`: AI サービスプロバイダー設定(API キーは Web UI で設定)
### 📥 インストール
```bash
# プロジェクトをクローン
git clone https://github.com/chatfire-AI/huobao-drama.git
cd huobao-drama
# Go依存関係をインストール
go mod download
# フロントエンド依存関係をインストール
cd web
npm install
cd ..
```
### 🎯 プロジェクトの起動
#### 方法 1: 開発モード(推奨)
**フロントエンドとバックエンドを分離、ホットリロード対応**
```bash
# ターミナル1: バックエンドサービスを起動
go run main.go
# ターミナル2: フロントエンド開発サーバーを起動
cd web
npm run dev
```
- フロントエンド: `http://localhost:3012`
- バックエンド API: `http://localhost:5678/api/v1`
- フロントエンドは API リクエストを自動的にバックエンドにプロキシ
#### 方法 2: シングルサービスモード
**バックエンドが API とフロントエンド静的ファイルの両方を提供**
```bash
# 1. フロントエンドをビルド
cd web
npm run build
cd ..
# 2. サービスを起動
go run main.go
```
アクセス: `http://localhost:5678`
### 🗄️ データベース初期化
データベーステーブルは初回起動時に自動作成されます(GORM AutoMigrate を使用)。手動マイグレーションは不要です。
---
## 📦 デプロイ
### ☁️ クラウドワンクリックデプロイ(推奨 3080Ti)
👉 [优云智算,一键部署](https://www.compshare.cn/images/fScvzK95NUk5?referral_code=8hUJOaWz3YzG64FI2OlCiB&ytag=GPU_YY_YX_GitHub_huobaoai)
> ⚠️ **注意**:クラウドデプロイを使用する場合は、データを速やかにローカルストレージに保存してください
---
### 🐳 Docker デプロイ(推奨)
#### 方法 1: Docker Compose(推奨)
#### 🚀 中国国内ネットワーク高速化(オプション)
中国国内のネットワーク環境では、Docker イメージのプルや依存関係のインストールが遅い場合があります。ミラーソースを設定することでビルドプロセスを高速化できます。
**ステップ 1: 環境変数ファイルを作成**
```bash
cp .env.example .env
```
**ステップ 2: `.env` ファイルを編集し、必要なミラーソースのコメントを解除**
```bash
# Docker Hub ミラーを有効化(推奨)
DOCKER_REGISTRY=docker.1ms.run/
# npm ミラーを有効化
NPM_REGISTRY=https://registry.npmmirror.com/
# Go プロキシを有効化
GO_PROXY=https://goproxy.cn,direct
# Alpine ミラーを有効化
ALPINE_MIRROR=mirrors.aliyun.com
```
**ステップ 3: docker compose でビルド(必須)**
```bash
docker compose build
```
> **重要な注意事項**:
>
> - ⚠️ `.env` ファイルのミラーソース設定を自動的に読み込むには `docker compose build` を使用する必要があります
> - ❌ `docker build` コマンドを使用する場合は、手動で `--build-arg` パラメータを渡す必要があります
> - ✅ 常に `docker compose build` を使用してビルドすることを推奨
**パフォーマンス比較**:
| 操作 | ミラー未設定 | ミラー設定後 |
| ------------------------ | -------------- | ------------ |
| ベースイメージのプル | 5-30 分 | 1-5 分 |
| npm 依存関係インストール | 失敗する可能性 | 高速成功 |
| Go 依存関係ダウンロード | 5-10 分 | 30 秒-1 分 |
> **注意**: 中国国外のユーザーはミラーソースを設定せず、デフォルト設定を使用してください。
```bash
# サービスを起動
docker-compose up -d
# ログを表示
docker-compose logs -f
# サービスを停止
docker-compose down
```
#### 方法 2: Docker コマンド
> **注意**: Linux ユーザーはホストサービスにアクセスするために `--add-host=host.docker.internal:host-gateway` を追加する必要があります
```bash
# Docker Hubから実行
docker run -d \
--name huobao-drama \
-p 5678:5678 \
-v $(pwd)/data:/app/data \
--restart unless-stopped \
huobao/huobao-drama:latest
# ログを表示
docker logs -f huobao-drama
```
**ローカルビルド**(オプション):
```bash
docker build -t huobao-drama:latest .
docker run -d --name huobao-drama -p 5678:5678 -v $(pwd)/data:/app/data huobao-drama:latest
```
**Docker デプロイの利点:**
- ✅ デフォルト設定ですぐに使用可能
- ✅ 環境の一貫性、依存関係の問題を回避
- ✅ ワンクリック起動、Go、Node.js、FFmpeg のインストール不要
- ✅ 移行とスケーリングが容易
- ✅ 自動ヘルスチェックと再起動
- ✅ ファイル権限の自動処理
#### 🔗 ホストサービスへのアクセス(Ollama/ローカルモデル)
コンテナは `http://host.docker.internal:ポート番号` を使用してホストサービスにアクセスするよう設定されています。
**設定手順:**
1. **ホストでサービスを起動(全インターフェースでリッスン)**
```bash
export OLLAMA_HOST=0.0.0.0:11434 && ollama serve
```
2. **フロントエンド AI サービス設定**
- Base URL: `http://host.docker.internal:11434/v1`
- Provider: `openai`
- Model: `qwen2.5:latest`
---
### 🏭 従来のデプロイ方法
#### 1. ビルド
```bash
# 1. フロントエンドをビルド
cd web
npm run build
cd ..
# 2. バックエンドをコンパイル
go build -o huobao-drama .
```
生成ファイル:
- `huobao-drama` - バックエンド実行ファイル
- `web/dist/` - フロントエンド静的ファイル(バックエンドに埋め込み)
#### 2. デプロイファイルの準備
サーバーにアップロードするファイル:
```
huobao-drama # バックエンド実行ファイル
configs/config.yaml # 設定ファイル
data/ # データディレクトリ(オプション、初回実行時に自動作成)
```
#### 3. サーバー設定
```bash
# ファイルをサーバーにアップロード
scp huobao-drama user@server:/opt/huobao-drama/
scp configs/config.yaml user@server:/opt/huobao-drama/configs/
# サーバーにSSH接続
ssh user@server
# 設定ファイルを編集
cd /opt/huobao-drama
vim configs/config.yaml
# modeをproductionに設定
# ドメインとストレージパスを設定
# データディレクトリを作成し権限を設定(重要!)
# 注意: YOUR_USERを実際にサービスを実行するユーザー名に置き換え(例: www-data、ubuntu、deploy)
sudo mkdir -p /opt/huobao-drama/data/storage
sudo chown -R YOUR_USER:YOUR_USER /opt/huobao-drama/data
sudo chmod -R 755 /opt/huobao-drama/data
# 実行権限を付与
chmod +x huobao-drama
# サービスを起動
./huobao-drama
```
#### 4. systemd でサービス管理
サービスファイル `/etc/systemd/system/huobao-drama.service` を作成:
```ini
[Unit]
Description=Huobao Drama Service
After=network.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/opt/huobao-drama
ExecStart=/opt/huobao-drama/huobao-drama
Restart=on-failure
RestartSec=10
# 環境変数(オプション)
# Environment="GIN_MODE=release"
[Install]
WantedBy=multi-user.target
```
サービスを起動:
```bash
sudo systemctl daemon-reload
sudo systemctl enable huobao-drama
sudo systemctl start huobao-drama
sudo systemctl status huobao-drama
```
**⚠️ よくある問題: SQLite 書き込み権限エラー**
`attempt to write a readonly database` エラーが発生した場合:
```bash
# 1. サービスを実行中のユーザーを確認
sudo systemctl status huobao-drama | grep "Main PID"
ps aux | grep huobao-drama
# 2. 権限を修正(YOUR_USERを実際のユーザー名に置き換え)
sudo chown -R YOUR_USER:YOUR_USER /opt/huobao-drama/data
sudo chmod -R 755 /opt/huobao-drama/data
# 3. 権限を確認
ls -la /opt/huobao-drama/data
# サービスを実行するユーザーが所有者として表示されるはず
# 4. サービスを再起動
sudo systemctl restart huobao-drama
```
**原因:**
- SQLite はデータベースファイル**と**そのディレクトリの両方に書き込み権限が必要
- ディレクトリ内に一時ファイル(例: `-wal`、`-journal`)を作成する必要がある
- **重要**: systemd の`User`がデータディレクトリの所有者と一致していることを確認
**一般的なユーザー名:**
- Ubuntu/Debian: `www-data`、`ubuntu`
- CentOS/RHEL: `nginx`、`apache`
- カスタムデプロイ: `deploy`、`app`、現在ログインしているユーザー
#### 5. Nginx リバースプロキシ
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 静的ファイルへの直接アクセス
location /static/ {
alias /opt/huobao-drama/data/storage/;
}
}
```
---
## 🎨 技術スタック
### バックエンド
- **言語**: Go 1.23+
- **Web フレームワーク**: Gin 1.9+
- **ORM**: GORM
- **データベース**: SQLite
- **ログ**: Zap
- **動画処理**: FFmpeg
- **AI サービス**: OpenAI、Gemini、Doubao など
### フロントエンド
- **フレームワーク**: Vue 3.4+
- **言語**: TypeScript 5+
- **ビルドツール**: Vite 5
- **UI コンポーネント**: Element Plus
- **CSS フレームワーク**: TailwindCSS
- **状態管理**: Pinia
- **ルーター**: Vue Router 4
### 開発ツール
- **パッケージ管理**: Go Modules、npm
- **コード規約**: ESLint、Prettier
- **バージョン管理**: Git
---
## 📝 よくある質問
### Q: Docker コンテナからホストの Ollama にアクセスするには?
A: Base URL として `http://host.docker.internal:11434/v1` を使用します。注意点:
1. ホストの Ollama は `0.0.0.0` でリッスンする必要があります: `export OLLAMA_HOST=0.0.0.0:11434 && ollama serve`
2. `docker run` を使用する Linux ユーザーは追加が必要: `--add-host=host.docker.internal:host-gateway`
詳細: [DOCKER_HOST_ACCESS.md](docs/DOCKER_HOST_ACCESS.md)
### Q: FFmpeg がインストールされていない、または見つからない?
A: FFmpeg がインストールされ、PATH 環境変数に含まれていることを確認してください。`ffmpeg -version` で確認。
### Q: フロントエンドがバックエンド API に接続できない?
A: バックエンドが実行中で、ポートが正しいか確認してください。開発モードでは、フロントエンドプロキシ設定は `web/vite.config.ts` にあります。
### Q: データベーステーブルが作成されない?
A: GORM は初回起動時にテーブルを自動作成します。ログでマイグレーション成功を確認してください。
---
## 📋 更新履歴
### v1.0.5 (2026-02-06)
#### 🎨 主要機能
- **🎭 グローバルスタイルシステム**:プロジェクト全体でスタイル選択をサポートする包括的なシステムを導入しました。ユーザーはドラマレベルでカスタムビジュアルスタイルを定義でき、キャラクター、シーン、ストーリーボードを含むすべてのAI生成コンテンツに自動的に適用され、制作全体で一貫した芸術的方向性を確保します。
- **✂️ 9グリッドシーケンス画像クロップ**:アクションシーケンス画像用のクロップツールを追加しました。3x3グリッドレイアウトから個別のフレームを抽出し、ビデオ生成用のファーストフレーム、ラストフレーム、またはキーフレームとして指定できるようになり、ショット構成と連続性においてより大きな柔軟性を提供します。
#### 🚀 機能強化
- **📐 アクションシーケンスグリッドの最適化**:9グリッドアクションシーケンス画像の視覚品質とレイアウトを改善し、間隔、配置、フレーム遷移を最適化しました。
- **🔧 手動グリッド組み立て**:2x2(4グリッド)、2x3(6グリッド)、3x3(9グリッド)レイアウトをサポートする手動グリッド構成ツールを導入し、個別のフレームからカスタムアクションシーケンスを作成できるようになりました。
- **🗑️ コンテンツ管理**:生成された画像とビデオの両方に削除機能を追加し、より良いアセット整理とストレージ管理を実現しました。
### v1.0.4 (2026-01-27)
#### 🚀 主要アップデート
- 生成コンテンツのローカルストレージ戦略を導入し、外部リソースリンクの有効期限切れリスクを効果的に軽減
- 参照画像の埋め込み転送用 Base64 エンコーディング方式を実装
- ショット切り替え時にショット画像プロンプト状態がリセットされない問題を修正
- ライブラリ動画追加時に動画の長さが 0 と表示される問題を修正
- シーンのエピソードへの移行機能を追加
#### 履歴データクリーニング
- 履歴データ処理用のマイグレーションスクリプトを追加。詳細な手順については [MIGRATE_README.md](MIGRATE_README.md) を参照してください
### v1.0.3 (2026-01-16)
#### 🚀 主要アップデート
- 純粋な Go SQLite ドライバー(`modernc.org/sqlite`)、`CGO_ENABLED=0` クロスプラットフォームコンパイルをサポート
- 並行性能を最適化(WAL モード)、"database is locked" エラーを解決
- ホストサービスへのアクセス用 `host.docker.internal` の Docker クロスプラットフォームサポート
- ドキュメントとデプロイガイドの簡素化
### v1.0.2 (2026-01-14)
#### 🐛 バグ修正 / 🔧 改善
- 動画生成 API レスポンスのパース問題を修正
- OpenAI Sora 動画エンドポイント設定を追加
- エラー処理とログ出力を最適化
---
## 🤝 コントリビューション
Issue と Pull Request を歓迎します!
1. このプロジェクトをフォーク
2. フィーチャーブランチを作成 (`git checkout -b feature/AmazingFeature`)
3. 変更をコミット (`git commit -m 'Add some AmazingFeature'`)
4. ブランチにプッシュ (`git push origin feature/AmazingFeature`)
5. Pull Request を作成
---
## API 設定サイト
2 分で設定完了: [API 集約サイト](https://api.chatfire.site/models)
---
## 👨💻 私たちについて
**AI 火宝 - AI スタジオ起業中**
- 🏠 **所在地**: 中国南京
- 🚀 **ステータス**: 起業中
- 📧 **Email**: [18550175439@163.com](mailto:18550175439@163.com)
- 💬 **WeChat**: dangbao1117 (個人 WeChat - 技術的な質問には対応しません)
- 🐙 **GitHub**: [https://github.com/chatfire-AI/huobao-drama](https://github.com/chatfire-AI/huobao-drama)
> _「AI に私たちのより創造的なことを手伝ってもらおう」_
## コミュニティグループ

- [Issue](../../issues)を提出
- プロジェクトメンテナにメール
---
<div align="center">
**⭐ このプロジェクトが役に立ったら、Star をお願いします!**
## Star 履歴
[](https://www.star-history.com/#chatfire-AI/huobao-drama&type=date&legend=top-left)
Made with ❤️ by Huobao Team
</div>
================================================
FILE: README.md
================================================
# 🎬 Huobao Drama - AI Short Drama Production Platform
<div align="center">
**Full-stack AI Short Drama Automation Platform Based on Go + Vue3**
[](https://golang.org)
[](https://vuejs.org)
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
[Features](#features) • [Quick Start](#quick-start) • [Deployment](#deployment)
[简体中文](README-CN.md) | [English](README.md) | [日本語](README-JA.md)
</div>
---
## 📖 About
Huobao Drama is an AI-powered short drama production platform that automates the entire workflow from script generation, character design, storyboarding to video composition.
火宝短剧商业版地址:[火宝短剧商业版](https://drama.chatfire.site/shortvideo)
火宝小说生成:[火宝小说生成](https://marketing.chatfire.site/huobao-novel/)
### 🎯 Core Features
- **🤖 AI-Driven**: Parse scripts using large language models to extract characters, scenes, and storyboards
- **🎨 Intelligent Creation**: AI-generated character portraits and scene backgrounds
- **📹 Video Generation**: Automatic storyboard video generation using text-to-video and image-to-video models
- **🔄 Complete Workflow**: End-to-end production workflow from idea to final video。
### 🛠️ Technical Architecture
Based on **DDD (Domain-Driven Design)** with clear layering:
```
├── API Layer (Gin HTTP)
├── Application Service Layer (Business Logic)
├── Domain Layer (Domain Models)
└── Infrastructure Layer (Database, External Services)
```
### 🎥 Demo Videos
Experience AI short drama generation:
<div align="center">
**Sample Work 1**
<video src="https://ffile.chatfire.site/cf/public/20260114094337396.mp4" controls width="640"></video>
**Sample Work 2**
<video src="https://ffile.chatfire.site/cf/public/fcede75e8aeafe22031dbf78f86285b8.mp4" controls width="640"></video>
[Watch Video 1](https://ffile.chatfire.site/cf/public/20260114094337396.mp4) | [Watch Video 2](https://ffile.chatfire.site/cf/public/fcede75e8aeafe22031dbf78f86285b8.mp4)
</div>
---
## ✨ Features
### 🎭 Character Management
- ✅ AI-generated character portraits
- ✅ Batch character generation
- ✅ Character image upload and management
### 🎬 Storyboard Production
- ✅ Automatic storyboard script generation
- ✅ Scene descriptions and shot design
- ✅ Storyboard image generation (text-to-image)
- ✅ Frame type selection (first frame/key frame/last frame/panel)
### 🎥 Video Generation
- ✅ Automatic image-to-video generation
- ✅ Video composition and editing
- ✅ Transition effects
### 📦 Asset Management
- ✅ Unified asset library management
- ✅ Local storage support
- ✅ Asset import/export
- ✅ Task progress tracking
---
## 🚀 Quick Start
### 📋 Prerequisites
| Software | Version | Description |
| ----------- | ------- | ------------------------------- |
| **Go** | 1.23+ | Backend runtime |
| **Node.js** | 18+ | Frontend build environment |
| **npm** | 9+ | Package manager |
| **FFmpeg** | 4.0+ | Video processing (**Required**) |
| **SQLite** | 3.x | Database (built-in) |
#### Installing FFmpeg
**macOS:**
```bash
brew install ffmpeg
```
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install ffmpeg
```
**Windows:**
Download from [FFmpeg Official Site](https://ffmpeg.org/download.html) and configure environment variables
Verify installation:
```bash
ffmpeg -version
```
### ⚙️ Configuration
Copy and edit the configuration file:
```bash
cp configs/config.example.yaml configs/config.yaml
vim configs/config.yaml
```
Configuration file format (`configs/config.yaml`):
```yaml
app:
name: "Huobao Drama API"
version: "1.0.0"
debug: true # Set to true for development, false for production
server:
port: 5678
host: "0.0.0.0"
cors_origins:
- "http://localhost:3012"
read_timeout: 600
write_timeout: 600
database:
type: "sqlite"
path: "./data/drama_generator.db"
max_idle: 10
max_open: 100
storage:
type: "local"
local_path: "./data/storage"
base_url: "http://localhost:5678/static"
ai:
default_text_provider: "openai"
default_image_provider: "openai"
default_video_provider: "doubao"
```
**Key Configuration Items:**
- `app.debug`: Debug mode switch (recommended true for development)
- `server.port`: Service port
- `server.cors_origins`: Allowed CORS origins for frontend
- `database.path`: SQLite database file path
- `storage.local_path`: Local file storage path
- `storage.base_url`: Static resource access URL
- `ai.default_*_provider`: AI service provider configuration (API keys configured in Web UI)
### 📥 Installation
```bash
# Clone the project
git clone https://github.com/chatfire-AI/huobao-drama.git
cd huobao-drama
# Install Go dependencies
go mod download
# Install frontend dependencies
cd web
npm install
cd ..
```
### 🎯 Starting the Project
#### Method 1: Development Mode (Recommended)
**Frontend and backend separation with hot reload**
```bash
# Terminal 1: Start backend service
go run main.go
# Terminal 2: Start frontend dev server
cd web
npm run dev
```
- Frontend: `http://localhost:3012`
- Backend API: `http://localhost:5678/api/v1`
- Frontend automatically proxies API requests to backend
#### Method 2: Single Service Mode
**Backend serves both API and frontend static files**
```bash
# 1. Build frontend
cd web
npm run build
cd ..
# 2. Start service
go run main.go
```
Access: `http://localhost:5678`
### 🗄️ Database Initialization
Database tables are automatically created on first startup (using GORM AutoMigrate), no manual migration needed.
---
## 📦 Deployment
### ☁️ Cloud One-Click Deployment (Recommended 3080Ti)
👉 [优云智算,一键部署](https://www.compshare.cn/images/fScvzK95NUk5?referral_code=8hUJOaWz3YzG64FI2OlCiB&ytag=GPU_YY_YX_GitHub_huobaoai)
> ⚠️ **Note**: Please save your data to local storage promptly when using cloud deployment
---
### 🐳 Docker Deployment (Recommended)
#### Method 1: Docker Compose (Recommended)
#### 🚀 China Network Acceleration (Optional)
If you are in China, pulling Docker images and installing dependencies may be slow. You can speed up the build process by configuring mirror sources.
**Step 1: Create environment variable file**
```bash
cp .env.example .env
```
**Step 2: Edit `.env` file and uncomment the mirror sources you need**
```bash
# Enable Docker Hub mirror (recommended)
DOCKER_REGISTRY=docker.1ms.run/
# Enable npm mirror
NPM_REGISTRY=https://registry.npmmirror.com/
# Enable Go proxy
GO_PROXY=https://goproxy.cn,direct
# Enable Alpine mirror
ALPINE_MIRROR=mirrors.aliyun.com
```
**Step 3: Build with docker compose (required)**
```bash
docker compose build
```
> **Important Note**:
>
> - ⚠️ You must use `docker compose build` to automatically load mirror source configurations from the `.env` file
> - ❌ If using `docker build` command, you need to manually pass `--build-arg` parameters
> - ✅ Always recommended to use `docker compose build` for building
**Performance Comparison**:
| Operation | Without Mirrors | With Mirrors |
| ---------------- | --------------- | ------------ |
| Pull base images | 5-30 minutes | 1-5 minutes |
| Install npm deps | May fail | Fast success |
| Download Go deps | 5-10 minutes | 30s-1 minute |
> **Note**: Users outside China should not configure mirror sources, use default settings.
```bash
# Start services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
```
#### Method 2: Docker Command
> **Note**: Linux users need to add `--add-host=host.docker.internal:host-gateway` to access host services
```bash
# Run from Docker Hub
docker run -d \
--name huobao-drama \
-p 5678:5678 \
-v $(pwd)/data:/app/data \
--restart unless-stopped \
huobao/huobao-drama:latest
# View logs
docker logs -f huobao-drama
```
**Local Build** (optional):
```bash
docker build -t huobao-drama:latest .
docker run -d --name huobao-drama -p 5678:5678 -v $(pwd)/data:/app/data huobao-drama:latest
```
**Docker Deployment Advantages:**
- ✅ Ready to use with default configuration
- ✅ Environment consistency, avoiding dependency issues
- ✅ One-click start, no need to install Go, Node.js, FFmpeg
- ✅ Easy to migrate and scale
- ✅ Automatic health checks and restarts
- ✅ Automatic file permission handling
#### 🔗 Accessing Host Services (Ollama/Local Models)
The container is configured to access host services using `http://host.docker.internal:PORT`.
**Configuration Steps:**
1. **Start service on host (listen on all interfaces)**
```bash
export OLLAMA_HOST=0.0.0.0:11434 && ollama serve
```
2. **Frontend AI Service Configuration**
- Base URL: `http://host.docker.internal:11434/v1`
- Provider: `openai`
- Model: `qwen2.5:latest`
---
### 🏭 Traditional Deployment
#### 1. Build
```bash
# 1. Build frontend
cd web
npm run build
cd ..
# 2. Compile backend
go build -o huobao-drama .
```
Generated files:
- `huobao-drama` - Backend executable
- `web/dist/` - Frontend static files (embedded in backend)
#### 2. Prepare Deployment Files
Files to upload to server:
```
huobao-drama # Backend executable
configs/config.yaml # Configuration file
data/ # Data directory (optional, auto-created on first run)
```
#### 3. Server Configuration
```bash
# Upload files to server
scp huobao-drama user@server:/opt/huobao-drama/
scp configs/config.yaml user@server:/opt/huobao-drama/configs/
# SSH to server
ssh user@server
# Modify configuration file
cd /opt/huobao-drama
vim configs/config.yaml
# Set mode to production
# Configure domain and storage path
# Create data directory and set permissions (Important!)
# Note: Replace YOUR_USER with actual user running the service (e.g., www-data, ubuntu, deploy)
sudo mkdir -p /opt/huobao-drama/data/storage
sudo chown -R YOUR_USER:YOUR_USER /opt/huobao-drama/data
sudo chmod -R 755 /opt/huobao-drama/data
# Grant execute permission
chmod +x huobao-drama
# Start service
./huobao-drama
```
#### 4. Manage Service with systemd
Create service file `/etc/systemd/system/huobao-drama.service`:
```ini
[Unit]
Description=Huobao Drama Service
After=network.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/opt/huobao-drama
ExecStart=/opt/huobao-drama/huobao-drama
Restart=on-failure
RestartSec=10
# Environment variables (optional)
# Environment="GIN_MODE=release"
[Install]
WantedBy=multi-user.target
```
Start service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable huobao-drama
sudo systemctl start huobao-drama
sudo systemctl status huobao-drama
```
**⚠️ Common Issue: SQLite Write Permission Error**
If you encounter `attempt to write a readonly database` error:
```bash
# 1. Check current user running the service
sudo systemctl status huobao-drama | grep "Main PID"
ps aux | grep huobao-drama
# 2. Fix permissions (replace YOUR_USER with actual username)
sudo chown -R YOUR_USER:YOUR_USER /opt/huobao-drama/data
sudo chmod -R 755 /opt/huobao-drama/data
# 3. Verify permissions
ls -la /opt/huobao-drama/data
# Should show owner as the user running the service
# 4. Restart service
sudo systemctl restart huobao-drama
```
**Reason:**
- SQLite requires write permission on both the database file **and** its directory
- Needs to create temporary files in the directory (e.g., `-wal`, `-journal`)
- **Key**: Ensure systemd `User` matches data directory owner
**Common Usernames:**
- Ubuntu/Debian: `www-data`, `ubuntu`
- CentOS/RHEL: `nginx`, `apache`
- Custom deployment: `deploy`, `app`, current logged-in user
#### 5. Nginx Reverse Proxy
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Direct access to static files
location /static/ {
alias /opt/huobao-drama/data/storage/;
}
}
```
---
## 🎨 Tech Stack
### Backend
- **Language**: Go 1.23+
- **Web Framework**: Gin 1.9+
- **ORM**: GORM
- **Database**: SQLite
- **Logging**: Zap
- **Video Processing**: FFmpeg
- **AI Services**: OpenAI, Gemini, Doubao, etc.
### Frontend
- **Framework**: Vue 3.4+
- **Language**: TypeScript 5+
- **Build Tool**: Vite 5
- **UI Components**: Element Plus
- **CSS Framework**: TailwindCSS
- **State Management**: Pinia
- **Router**: Vue Router 4
### Development Tools
- **Package Management**: Go Modules, npm
- **Code Standards**: ESLint, Prettier
- **Version Control**: Git
---
## 📝 FAQ
### Q: How can Docker containers access Ollama on the host?
A: Use `http://host.docker.internal:11434/v1` as Base URL. Note two things:
1. Host Ollama needs to listen on `0.0.0.0`: `export OLLAMA_HOST=0.0.0.0:11434 && ollama serve`
2. Linux users using `docker run` need to add: `--add-host=host.docker.internal:host-gateway`
See: [DOCKER_HOST_ACCESS.md](docs/DOCKER_HOST_ACCESS.md)
### Q: FFmpeg not installed or not found?
A: Ensure FFmpeg is installed and in the PATH environment variable. Verify with `ffmpeg -version`.
### Q: Frontend cannot connect to backend API?
A: Check if backend is running and port is correct. In development mode, frontend proxy config is in `web/vite.config.ts`.
### Q: Database tables not created?
A: GORM automatically creates tables on first startup, check logs to confirm migration success.
---
## 📋 Changelog
### v1.0.5 (2026-02-06)
#### 🎨 Major Features
- **🎭 Global Style System**: Introduced comprehensive style selection support across the entire project. Users can now define custom visual styles at the drama level, which automatically applies to all AI-generated content including characters, scenes, and storyboards, ensuring consistent artistic direction throughout the production.
- **✂️ Nine-Grid Sequence Image Cropping**: Added cropping tool for action sequence images. Users can now extract individual frames from 3x3 grid layouts and designate them as first frames, last frames, or keyframes for video generation, providing greater flexibility in shot composition and continuity.
#### 🚀 Enhancements
- **📐 Optimized Action Sequence Grid**: Enhanced the visual quality and layout of nine-grid action sequence images with improved spacing, alignment, and frame transitions.
- **🔧 Manual Grid Assembly**: Introduced manual grid composition tools supporting 2x2 (four-grid), 2x3 (six-grid), and 3x3 (nine-grid) layouts, allowing users to create custom action sequences from individual frames.
- **🗑️ Content Management**: Added delete functionality for both generated images and videos, enabling better asset organization and storage management.
### v1.0.4 (2026-01-27)
#### 🚀 Major Updates
- Introduced local storage strategy for generated content caching, effectively mitigating external resource link expiration risks
- Implemented Base64 encoding for embedded reference image transmission
- Fixed issue where shot image prompt state was not reset when switching shots
- Fixed issue where video duration displayed as 0 when adding library videos
- Added scene migration to episodes
#### Historical Data Migration
- Added migration script for processing historical data. For detailed instructions, please refer to [MIGRATE_README.md](MIGRATE_README.md)
### v1.0.3 (2026-01-16)
#### 🚀 Major Updates
- Pure Go SQLite driver (`modernc.org/sqlite`), supports `CGO_ENABLED=0` cross-platform compilation
- Optimized concurrency performance (WAL mode), resolved "database is locked" errors
- Docker cross-platform support for `host.docker.internal` to access host services
- Streamlined documentation and deployment guides
### v1.0.2 (2026-01-14)
#### 🐛 Bug Fixes / 🔧 Improvements
- Fixed video generation API response parsing issues
- Added OpenAI Sora video endpoint configuration
- Optimized error handling and logging
---
## 🤝 Contributing
Issues and Pull Requests are welcome!
1. Fork this project
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
---
## API Configuration Site
Configure in 2 minutes: [API Aggregation Site](https://api.chatfire.site/models)
---
## 👨💻 About Us
**AI Huobao - AI Studio Startup**
- 🏠 **Location**: Nanjing, China
- 🚀 **Status**: Startup in Progress
- 📧 **Email**: [18550175439@163.com](mailto:18550175439@163.com)
- 🐙 **GitHub**: [https://github.com/chatfire-AI/huobao-drama](https://github.com/chatfire-AI/huobao-drama)
> _"Let AI help us do more creative things"_
## Community Group

- Submit [Issue](../../issues)
- Email project maintainers
---
<div align="center">
**⭐ If this project helps you, please give it a Star!**
## Star History
[](https://www.star-history.com/#chatfire-AI/huobao-drama&type=date&legend=top-left)
Made with ❤️ by Huobao Team
</div>
================================================
FILE: api/handlers/ai_config.go
================================================
package handlers
import (
"strconv"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AIConfigHandler struct {
aiService *services.AIService
log *logger.Logger
}
func NewAIConfigHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *AIConfigHandler {
return &AIConfigHandler{
aiService: services.NewAIService(db, log),
log: log,
}
}
func (h *AIConfigHandler) CreateConfig(c *gin.Context) {
var req services.CreateAIConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
config, err := h.aiService.CreateConfig(&req)
if err != nil {
response.InternalError(c, "创建失败")
return
}
response.Created(c, config)
}
func (h *AIConfigHandler) GetConfig(c *gin.Context) {
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的配置ID")
return
}
config, err := h.aiService.GetConfig(uint(configID))
if err != nil {
if err.Error() == "config not found" {
response.NotFound(c, "配置不存在")
return
}
response.InternalError(c, "获取失败")
return
}
response.Success(c, config)
}
func (h *AIConfigHandler) ListConfigs(c *gin.Context) {
serviceType := c.Query("service_type")
configs, err := h.aiService.ListConfigs(serviceType)
if err != nil {
response.InternalError(c, "获取列表失败")
return
}
response.Success(c, configs)
}
func (h *AIConfigHandler) UpdateConfig(c *gin.Context) {
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的配置ID")
return
}
var req services.UpdateAIConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
config, err := h.aiService.UpdateConfig(uint(configID), &req)
if err != nil {
if err.Error() == "config not found" {
response.NotFound(c, "配置不存在")
return
}
response.InternalError(c, "更新失败")
return
}
response.Success(c, config)
}
func (h *AIConfigHandler) DeleteConfig(c *gin.Context) {
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的配置ID")
return
}
if err := h.aiService.DeleteConfig(uint(configID)); err != nil {
if err.Error() == "config not found" {
response.NotFound(c, "配置不存在")
return
}
response.InternalError(c, "删除失败")
return
}
response.Success(c, gin.H{"message": "删除成功"})
}
func (h *AIConfigHandler) TestConnection(c *gin.Context) {
var req services.TestConnectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.aiService.TestConnection(&req); err != nil {
response.BadRequest(c, "连接测试失败: "+err.Error())
return
}
response.Success(c, gin.H{"message": "连接测试成功"})
}
================================================
FILE: api/handlers/asset.go
================================================
package handlers
import (
"strconv"
"strings"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AssetHandler struct {
assetService *services.AssetService
log *logger.Logger
}
func NewAssetHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *AssetHandler {
return &AssetHandler{
assetService: services.NewAssetService(db, log),
log: log,
}
}
func (h *AssetHandler) CreateAsset(c *gin.Context) {
var req services.CreateAssetRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
asset, err := h.assetService.CreateAsset(&req)
if err != nil {
h.log.Errorw("Failed to create asset", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, asset)
}
func (h *AssetHandler) UpdateAsset(c *gin.Context) {
assetID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
var req services.UpdateAssetRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
asset, err := h.assetService.UpdateAsset(uint(assetID), &req)
if err != nil {
h.log.Errorw("Failed to update asset", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, asset)
}
func (h *AssetHandler) GetAsset(c *gin.Context) {
assetID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
asset, err := h.assetService.GetAsset(uint(assetID))
if err != nil {
response.NotFound(c, "素材不存在")
return
}
response.Success(c, asset)
}
func (h *AssetHandler) ListAssets(c *gin.Context) {
var dramaID *string
if dramaIDStr := c.Query("drama_id"); dramaIDStr != "" {
dramaID = &dramaIDStr
}
var episodeID *uint
if episodeIDStr := c.Query("episode_id"); episodeIDStr != "" {
if id, err := strconv.ParseUint(episodeIDStr, 10, 32); err == nil {
uid := uint(id)
episodeID = &uid
}
}
var storyboardID *uint
if storyboardIDStr := c.Query("storyboard_id"); storyboardIDStr != "" {
if id, err := strconv.ParseUint(storyboardIDStr, 10, 32); err == nil {
uid := uint(id)
storyboardID = &uid
}
}
var assetType *models.AssetType
if typeStr := c.Query("type"); typeStr != "" {
t := models.AssetType(typeStr)
assetType = &t
}
var isFavorite *bool
if favoriteStr := c.Query("is_favorite"); favoriteStr != "" {
if favoriteStr == "true" {
fav := true
isFavorite = &fav
} else if favoriteStr == "false" {
fav := false
isFavorite = &fav
}
}
var tagIDs []uint
if tagIDsStr := c.Query("tag_ids"); tagIDsStr != "" {
for _, idStr := range strings.Split(tagIDsStr, ",") {
if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil {
tagIDs = append(tagIDs, uint(id))
}
}
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
req := &services.ListAssetsRequest{
DramaID: dramaID,
EpisodeID: episodeID,
StoryboardID: storyboardID,
Type: assetType,
Category: c.Query("category"),
TagIDs: tagIDs,
IsFavorite: isFavorite,
Search: c.Query("search"),
Page: page,
PageSize: pageSize,
}
assets, total, err := h.assetService.ListAssets(req)
if err != nil {
h.log.Errorw("Failed to list assets", "error", err)
response.InternalError(c, err.Error())
return
}
response.SuccessWithPagination(c, assets, total, page, pageSize)
}
func (h *AssetHandler) DeleteAsset(c *gin.Context) {
assetID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
if err := h.assetService.DeleteAsset(uint(assetID)); err != nil {
h.log.Errorw("Failed to delete asset", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
func (h *AssetHandler) ImportFromImageGen(c *gin.Context) {
imageGenID, err := strconv.ParseUint(c.Param("image_gen_id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
asset, err := h.assetService.ImportFromImageGen(uint(imageGenID))
if err != nil {
h.log.Errorw("Failed to import from image gen", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, asset)
}
func (h *AssetHandler) ImportFromVideoGen(c *gin.Context) {
videoGenID, err := strconv.ParseUint(c.Param("video_gen_id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
asset, err := h.assetService.ImportFromVideoGen(uint(videoGenID))
if err != nil {
h.log.Errorw("Failed to import from video gen", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, asset)
}
================================================
FILE: api/handlers/audio_extraction.go
================================================
package handlers
import (
"net/http"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/logger"
"github.com/gin-gonic/gin"
)
type AudioExtractionHandler struct {
service *services.AudioExtractionService
log *logger.Logger
dataDir string
}
func NewAudioExtractionHandler(log *logger.Logger, dataDir string) *AudioExtractionHandler {
return &AudioExtractionHandler{
service: services.NewAudioExtractionService(log),
log: log,
dataDir: dataDir,
}
}
// ExtractAudio 提取单个视频的音频
// @Summary 提取视频音频
// @Description 从视频URL中提取音频轨道
// @Tags Audio
// @Accept json
// @Produce json
// @Param request body services.ExtractAudioRequest true "提取请求"
// @Success 200 {object} services.ExtractAudioResponse
// @Router /api/audio/extract [post]
func (h *AudioExtractionHandler) ExtractAudio(c *gin.Context) {
var req services.ExtractAudioRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.log.Errorw("Invalid request body", "error", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
h.log.Infow("Received audio extraction request", "video_url", req.VideoURL)
result, err := h.service.ExtractAudio(req.VideoURL, h.dataDir)
if err != nil {
h.log.Errorw("Failed to extract audio", "error", err, "video_url", req.VideoURL)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
type BatchExtractAudioRequest struct {
VideoURLs []string `json:"video_urls" binding:"required,min=1"`
}
// BatchExtractAudio 批量提取音频
// @Summary 批量提取视频音频
// @Description 从多个视频URL中提取音频轨道
// @Tags Audio
// @Accept json
// @Produce json
// @Param request body BatchExtractAudioRequest true "批量提取请求"
// @Success 200 {array} services.ExtractAudioResponse
// @Router /api/audio/extract/batch [post]
func (h *AudioExtractionHandler) BatchExtractAudio(c *gin.Context) {
var req BatchExtractAudioRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.log.Errorw("Invalid request body", "error", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
h.log.Infow("Received batch audio extraction request", "count", len(req.VideoURLs))
results, err := h.service.BatchExtractAudio(req.VideoURLs, h.dataDir)
if err != nil {
h.log.Errorw("Failed to batch extract audio", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"total": len(results),
})
}
================================================
FILE: api/handlers/character_batch.go
================================================
package handlers
import (
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
)
// BatchGenerateCharacterImages 批量生成角色图片
func (h *CharacterLibraryHandler) BatchGenerateCharacterImages(c *gin.Context) {
var req struct {
CharacterIDs []string `json:"character_ids" binding:"required,min=1"`
Model string `json:"model"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// 限制批量生成数量
if len(req.CharacterIDs) > 10 {
response.BadRequest(c, "单次最多生成10个角色")
return
}
// 异步批量生成
go h.libraryService.BatchGenerateCharacterImages(req.CharacterIDs, h.imageService, req.Model)
response.Success(c, gin.H{
"message": "批量生成任务已提交",
"count": len(req.CharacterIDs),
})
}
================================================
FILE: api/handlers/character_library.go
================================================
package handlers
import (
"strconv"
services2 "github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/infrastructure/storage"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type CharacterLibraryHandler struct {
libraryService *services2.CharacterLibraryService
imageService *services2.ImageGenerationService
log *logger.Logger
}
func NewCharacterLibraryHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services2.ResourceTransferService, localStorage *storage.LocalStorage) *CharacterLibraryHandler {
return &CharacterLibraryHandler{
libraryService: services2.NewCharacterLibraryService(db, log, cfg),
imageService: services2.NewImageGenerationService(db, cfg, transferService, localStorage, log),
log: log,
}
}
// ListLibraryItems 获取角色库列表
func (h *CharacterLibraryHandler) ListLibraryItems(c *gin.Context) {
var query services2.CharacterLibraryQuery
if err := c.ShouldBindQuery(&query); err != nil {
response.BadRequest(c, err.Error())
return
}
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 || query.PageSize > 100 {
query.PageSize = 20
}
items, total, err := h.libraryService.ListLibraryItems(&query)
if err != nil {
h.log.Errorw("Failed to list library items", "error", err)
response.InternalError(c, "获取角色库失败")
return
}
response.SuccessWithPagination(c, items, total, query.Page, query.PageSize)
}
// CreateLibraryItem 添加到角色库
func (h *CharacterLibraryHandler) CreateLibraryItem(c *gin.Context) {
var req services2.CreateLibraryItemRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
item, err := h.libraryService.CreateLibraryItem(&req)
if err != nil {
h.log.Errorw("Failed to create library item", "error", err)
response.InternalError(c, "添加到角色库失败")
return
}
response.Created(c, item)
}
// GetLibraryItem 获取角色库项详情
func (h *CharacterLibraryHandler) GetLibraryItem(c *gin.Context) {
itemID := c.Param("id")
item, err := h.libraryService.GetLibraryItem(itemID)
if err != nil {
if err.Error() == "library item not found" {
response.NotFound(c, "角色库项不存在")
return
}
h.log.Errorw("Failed to get library item", "error", err)
response.InternalError(c, "获取失败")
return
}
response.Success(c, item)
}
// DeleteLibraryItem 删除角色库项
func (h *CharacterLibraryHandler) DeleteLibraryItem(c *gin.Context) {
itemID := c.Param("id")
if err := h.libraryService.DeleteLibraryItem(itemID); err != nil {
if err.Error() == "library item not found" {
response.NotFound(c, "角色库项不存在")
return
}
h.log.Errorw("Failed to delete library item", "error", err)
response.InternalError(c, "删除失败")
return
}
response.Success(c, gin.H{"message": "删除成功"})
}
// UploadCharacterImage 上传角色图片
func (h *CharacterLibraryHandler) UploadCharacterImage(c *gin.Context) {
characterID := c.Param("id")
// TODO: 处理文件上传
// 这里需要实现文件上传逻辑,保存到OSS或本地
// 暂时使用简单的实现
var req struct {
ImageURL string `json:"image_url" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.libraryService.UploadCharacterImage(characterID, req.ImageURL); err != nil {
if err.Error() == "character not found" {
response.NotFound(c, "角色不存在")
return
}
if err.Error() == "unauthorized" {
response.Forbidden(c, "无权限")
return
}
h.log.Errorw("Failed to upload character image", "error", err)
response.InternalError(c, "上传失败")
return
}
response.Success(c, gin.H{"message": "上传成功"})
}
// ApplyLibraryItemToCharacter 从角色库应用形象
func (h *CharacterLibraryHandler) ApplyLibraryItemToCharacter(c *gin.Context) {
characterID := c.Param("id")
var req struct {
LibraryItemID string `json:"library_item_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.libraryService.ApplyLibraryItemToCharacter(characterID, req.LibraryItemID); err != nil {
if err.Error() == "library item not found" {
response.NotFound(c, "角色库项不存在")
return
}
if err.Error() == "character not found" {
response.NotFound(c, "角色不存在")
return
}
if err.Error() == "unauthorized" {
response.Forbidden(c, "无权限")
return
}
h.log.Errorw("Failed to apply library item", "error", err)
response.InternalError(c, "应用失败")
return
}
response.Success(c, gin.H{"message": "应用成功"})
}
// AddCharacterToLibrary 将角色添加到角色库
func (h *CharacterLibraryHandler) AddCharacterToLibrary(c *gin.Context) {
characterID := c.Param("id")
var req struct {
Category *string `json:"category"`
}
if err := c.ShouldBindJSON(&req); err != nil {
// 允许空body
req.Category = nil
}
item, err := h.libraryService.AddCharacterToLibrary(characterID, req.Category)
if err != nil {
if err.Error() == "character not found" {
response.NotFound(c, "角色不存在")
return
}
if err.Error() == "unauthorized" {
response.Forbidden(c, "无权限")
return
}
if err.Error() == "character has no image" {
response.BadRequest(c, "角色还没有形象图片")
return
}
h.log.Errorw("Failed to add character to library", "error", err)
response.InternalError(c, "添加失败")
return
}
response.Created(c, item)
}
// UpdateCharacter 更新角色信息
func (h *CharacterLibraryHandler) UpdateCharacter(c *gin.Context) {
characterID := c.Param("id")
var req services2.UpdateCharacterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.libraryService.UpdateCharacter(characterID, &req); err != nil {
if err.Error() == "character not found" {
response.NotFound(c, "角色不存在")
return
}
if err.Error() == "unauthorized" {
response.Forbidden(c, "无权限")
return
}
h.log.Errorw("Failed to update character", "error", err)
response.InternalError(c, "更新失败")
return
}
response.Success(c, gin.H{"message": "更新成功"})
}
// DeleteCharacter 删除单个角色
func (h *CharacterLibraryHandler) DeleteCharacter(c *gin.Context) {
characterIDStr := c.Param("id")
characterID, err := strconv.ParseUint(characterIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "无效的角色ID")
return
}
if err := h.libraryService.DeleteCharacter(uint(characterID)); err != nil {
h.log.Errorw("Failed to delete character", "error", err, "id", characterID)
if err.Error() == "character not found" {
response.NotFound(c, "角色不存在")
return
}
if err.Error() == "unauthorized" {
response.Forbidden(c, "无权删除此角色")
return
}
response.InternalError(c, "删除失败")
return
}
response.Success(c, gin.H{"message": "角色已删除"})
}
// ExtractCharacters 从剧本提取角色
func (h *CharacterLibraryHandler) ExtractCharacters(c *gin.Context) {
episodeIDStr := c.Param("episode_id")
episodeID, err := strconv.ParseUint(episodeIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid episode_id")
return
}
taskID, err := h.libraryService.ExtractCharactersFromScript(uint(episodeID))
if err != nil {
h.log.Errorw("Failed to extract characters", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"task_id": taskID, "message": "角色提取任务已提交"})
}
================================================
FILE: api/handlers/character_library_gen.go
================================================
package handlers
import (
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
)
// GenerateCharacterImage AI生成角色形象
func (h *CharacterLibraryHandler) GenerateCharacterImage(c *gin.Context) {
characterID := c.Param("id")
// 获取请求体中的model和style参数
var req struct {
Model string `json:"model"`
Style string `json:"style"`
}
c.ShouldBindJSON(&req)
imageGen, err := h.libraryService.GenerateCharacterImage(characterID, h.imageService, req.Model, req.Style)
if err != nil {
if err.Error() == "character not found" {
response.NotFound(c, "角色不存在")
return
}
if err.Error() == "unauthorized" {
response.Forbidden(c, "无权限")
return
}
h.log.Errorw("Failed to generate character image", "error", err)
response.InternalError(c, "生成失败")
return
}
response.Success(c, gin.H{
"message": "角色图片生成已启动",
"image_generation": imageGen,
})
}
================================================
FILE: api/handlers/drama.go
================================================
package handlers
import (
"encoding/json"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type DramaHandler struct {
db *gorm.DB
dramaService *services.DramaService
videoMergeService *services.VideoMergeService
log *logger.Logger
}
func NewDramaHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services.ResourceTransferService) *DramaHandler {
return &DramaHandler{
db: db,
dramaService: services.NewDramaService(db, cfg, log),
videoMergeService: services.NewVideoMergeService(db, transferService, cfg.Storage.LocalPath, cfg.Storage.BaseURL, log),
log: log,
}
}
func (h *DramaHandler) CreateDrama(c *gin.Context) {
var req services.CreateDramaRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
drama, err := h.dramaService.CreateDrama(&req)
if err != nil {
response.InternalError(c, "创建失败")
return
}
response.Created(c, drama)
}
func (h *DramaHandler) GetDrama(c *gin.Context) {
dramaID := c.Param("id")
drama, err := h.dramaService.GetDrama(dramaID)
if err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "获取失败")
return
}
response.Success(c, drama)
}
func (h *DramaHandler) ListDramas(c *gin.Context) {
var query services.DramaListQuery
if err := c.ShouldBindQuery(&query); err != nil {
response.BadRequest(c, err.Error())
return
}
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 || query.PageSize > 100 {
query.PageSize = 20
}
dramas, total, err := h.dramaService.ListDramas(&query)
if err != nil {
response.InternalError(c, "获取列表失败")
return
}
response.SuccessWithPagination(c, dramas, total, query.Page, query.PageSize)
}
func (h *DramaHandler) UpdateDrama(c *gin.Context) {
dramaID := c.Param("id")
var req services.UpdateDramaRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
drama, err := h.dramaService.UpdateDrama(dramaID, &req)
if err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "更新失败")
return
}
response.Success(c, drama)
}
func (h *DramaHandler) DeleteDrama(c *gin.Context) {
dramaID := c.Param("id")
if err := h.dramaService.DeleteDrama(dramaID); err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "删除失败")
return
}
response.Success(c, gin.H{"message": "删除成功"})
}
func (h *DramaHandler) GetDramaStats(c *gin.Context) {
stats, err := h.dramaService.GetDramaStats()
if err != nil {
response.InternalError(c, "获取统计失败")
return
}
response.Success(c, stats)
}
func (h *DramaHandler) SaveOutline(c *gin.Context) {
dramaID := c.Param("id")
var req services.SaveOutlineRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.dramaService.SaveOutline(dramaID, &req); err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "保存失败")
return
}
response.Success(c, gin.H{"message": "保存成功"})
}
func (h *DramaHandler) GetCharacters(c *gin.Context) {
dramaID := c.Param("id")
episodeID := c.Query("episode_id") // 可选:如果提供则只返回该章节的角色
var episodeIDPtr *string
if episodeID != "" {
episodeIDPtr = &episodeID
}
characters, err := h.dramaService.GetCharacters(dramaID, episodeIDPtr)
if err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
if err.Error() == "episode not found" {
response.NotFound(c, "章节不存在")
return
}
response.InternalError(c, "获取角色失败")
return
}
response.Success(c, characters)
}
func (h *DramaHandler) SaveCharacters(c *gin.Context) {
dramaID := c.Param("id")
var req services.SaveCharactersRequest
// 先尝试正常绑定JSON
if err := c.ShouldBindJSON(&req); err != nil {
// 如果绑定失败,检查是否是因为characters字段是字符串而不是数组
var rawReq map[string]interface{}
if err := c.ShouldBindJSON(&rawReq); err != nil {
// 如果连rawReq都绑定失败,直接返回错误
response.BadRequest(c, err.Error())
return
}
// 检查characters字段类型
if charField, ok := rawReq["characters"]; ok {
if charStr, ok := charField.(string); ok {
// 如果characters是字符串,尝试解析为JSON数组
var characters []models.Character
if err := json.Unmarshal([]byte(charStr), &characters); err != nil {
// 解析失败,返回错误
response.BadRequest(c, "characters字段格式错误,需要JSON数组或字符串格式的JSON数组")
return
}
// 手动构造请求对象
req.Characters = characters
// 处理episode_id字段
if epID, ok := rawReq["episode_id"]; ok {
if epIDStr, ok := epID.(float64); ok {
epIDUint := uint(epIDStr)
req.EpisodeID = &epIDUint
}
}
} else {
// 如果characters不是字符串,直接返回原始错误
response.BadRequest(c, err.Error())
return
}
} else {
// 如果没有characters字段,返回原始错误
response.BadRequest(c, err.Error())
return
}
}
if err := h.dramaService.SaveCharacters(dramaID, &req); err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "保存失败")
return
}
response.Success(c, gin.H{"message": "保存成功"})
}
func (h *DramaHandler) SaveEpisodes(c *gin.Context) {
dramaID := c.Param("id")
var req services.SaveEpisodesRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.dramaService.SaveEpisodes(dramaID, &req); err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "保存失败")
return
}
response.Success(c, gin.H{"message": "保存成功"})
}
func (h *DramaHandler) SaveProgress(c *gin.Context) {
dramaID := c.Param("id")
var req services.SaveProgressRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.dramaService.SaveProgress(dramaID, &req); err != nil {
if err.Error() == "drama not found" {
response.NotFound(c, "剧本不存在")
return
}
response.InternalError(c, "保存失败")
return
}
response.Success(c, gin.H{"message": "保存成功"})
}
// FinalizeEpisode 完成集数制作(触发视频合成)
func (h *DramaHandler) FinalizeEpisode(c *gin.Context) {
episodeID := c.Param("episode_id")
if episodeID == "" {
response.BadRequest(c, "episode_id不能为空")
return
}
// 尝试读取时间线数据(可选)
var timelineData *services.FinalizeEpisodeRequest
if err := c.ShouldBindJSON(&timelineData); err != nil {
// 如果没有请求体或解析失败,使用nil(将使用默认场景顺序)
h.log.Warnw("No timeline data provided, will use default scene order", "error", err)
timelineData = nil
} else if timelineData != nil {
h.log.Infow("Received timeline data", "clips_count", len(timelineData.Clips), "episode_id", episodeID)
}
// 触发视频合成任务
result, err := h.videoMergeService.FinalizeEpisode(episodeID, timelineData)
if err != nil {
h.log.Errorw("Failed to finalize episode", "error", err, "episode_id", episodeID)
response.InternalError(c, err.Error())
return
}
response.Success(c, result)
}
// DownloadEpisodeVideo 下载剧集视频
func (h *DramaHandler) DownloadEpisodeVideo(c *gin.Context) {
episodeID := c.Param("episode_id")
if episodeID == "" {
response.BadRequest(c, "episode_id不能为空")
return
}
// 查询episode
var episode models.Episode
if err := h.db.Preload("Drama").Where("id = ?", episodeID).First(&episode).Error; err != nil {
response.NotFound(c, "剧集不存在")
return
}
// 检查是否有视频
if episode.VideoURL == nil || *episode.VideoURL == "" {
response.BadRequest(c, "该剧集还没有生成视频")
return
}
// 返回视频URL,让前端重定向下载
c.JSON(200, gin.H{
"video_url": *episode.VideoURL,
"title": episode.Title,
"episode_number": episode.EpisodeNum,
})
}
================================================
FILE: api/handlers/frame_prompt.go
================================================
package handlers
import (
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
)
// FramePromptHandler 处理帧提示词生成请求
type FramePromptHandler struct {
framePromptService *services.FramePromptService
log *logger.Logger
}
// NewFramePromptHandler 创建帧提示词处理器
func NewFramePromptHandler(framePromptService *services.FramePromptService, log *logger.Logger) *FramePromptHandler {
return &FramePromptHandler{
framePromptService: framePromptService,
log: log,
}
}
// GenerateFramePrompt 生成指定类型的帧提示词
// POST /api/v1/storyboards/:id/frame-prompt
func (h *FramePromptHandler) GenerateFramePrompt(c *gin.Context) {
storyboardID := c.Param("id")
var req struct {
FrameType string `json:"frame_type"`
PanelCount int `json:"panel_count"`
Model string `json:"model"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
serviceReq := services.GenerateFramePromptRequest{
StoryboardID: storyboardID,
FrameType: services.FrameType(req.FrameType),
PanelCount: req.PanelCount,
}
// 直接调用服务层的异步方法,该方法会创建任务并返回任务ID
taskID, err := h.framePromptService.GenerateFramePrompt(serviceReq, req.Model)
if err != nil {
h.log.Errorw("Failed to generate frame prompt", "error", err)
response.InternalError(c, err.Error())
return
}
// 立即返回任务ID
response.Success(c, gin.H{
"task_id": taskID,
"status": "pending",
"message": "帧提示词生成任务已创建,正在后台处理...",
})
}
================================================
FILE: api/handlers/frame_prompt_query.go
================================================
package handlers
import (
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// GetStoryboardFramePrompts 查询镜头的所有帧提示词
// GET /api/v1/storyboards/:id/frame-prompts
func GetStoryboardFramePrompts(db *gorm.DB, log *logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
storyboardID := c.Param("id")
var framePrompts []models.FramePrompt
if err := db.Where("storyboard_id = ?", storyboardID).
Order("created_at DESC").
Find(&framePrompts).Error; err != nil {
log.Errorw("Failed to query frame prompts", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{
"frame_prompts": framePrompts,
})
}
}
================================================
FILE: api/handlers/image_generation.go
================================================
package handlers
import (
"strconv"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/infrastructure/storage"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ImageGenerationHandler struct {
imageService *services.ImageGenerationService
taskService *services.TaskService
log *logger.Logger
config *config.Config
db *gorm.DB
}
func NewImageGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services.ResourceTransferService, localStorage *storage.LocalStorage) *ImageGenerationHandler {
return &ImageGenerationHandler{
imageService: services.NewImageGenerationService(db, cfg, transferService, localStorage, log),
taskService: services.NewTaskService(db, log),
log: log,
config: cfg,
db: db,
}
}
func (h *ImageGenerationHandler) GenerateImage(c *gin.Context) {
var req services.GenerateImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
imageGen, err := h.imageService.GenerateImage(&req)
if err != nil {
h.log.Errorw("Failed to generate image", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, imageGen)
}
func (h *ImageGenerationHandler) GenerateImagesForScene(c *gin.Context) {
sceneID := c.Param("scene_id")
images, err := h.imageService.GenerateImagesForScene(sceneID)
if err != nil {
h.log.Errorw("Failed to generate images for scene", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, images)
}
func (h *ImageGenerationHandler) GetBackgroundsForEpisode(c *gin.Context) {
episodeID := c.Param("episode_id")
backgrounds, err := h.imageService.GetScencesForEpisode(episodeID)
if err != nil {
h.log.Errorw("Failed to get backgrounds", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, backgrounds)
}
func (h *ImageGenerationHandler) ExtractBackgroundsForEpisode(c *gin.Context) {
episodeID := c.Param("episode_id")
// 接收可选的 model 和 style 参数
var req struct {
Model string `json:"model"`
Style string `json:"style"`
}
if err := c.ShouldBindJSON(&req); err != nil {
// 如果没有提供body或者解析失败,使用空字符串(使用默认模型和风格)
req.Model = ""
req.Style = ""
}
// 如果style为空,从episode获取drama的style
if req.Style == "" {
var episode models.Episode
if err := h.db.Preload("Drama").First(&episode, episodeID).Error; err == nil {
req.Style = episode.Drama.Style
}
}
// 直接调用服务层的异步方法,该方法会创建任务并返回任务ID
taskID, err := h.imageService.ExtractBackgroundsForEpisode(episodeID, req.Model, req.Style)
if err != nil {
h.log.Errorw("Failed to extract backgrounds", "error", err, "episode_id", episodeID)
response.InternalError(c, err.Error())
return
}
// 立即返回任务ID
response.Success(c, gin.H{
"task_id": taskID,
"status": "pending",
"message": "场景提取任务已创建,正在后台处理...",
})
}
func (h *ImageGenerationHandler) BatchGenerateForEpisode(c *gin.Context) {
episodeID := c.Param("episode_id")
images, err := h.imageService.BatchGenerateImagesForEpisode(episodeID)
if err != nil {
h.log.Errorw("Failed to batch generate images", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, images)
}
func (h *ImageGenerationHandler) GetImageGeneration(c *gin.Context) {
imageGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
imageGen, err := h.imageService.GetImageGeneration(uint(imageGenID))
if err != nil {
response.NotFound(c, "图片生成记录不存在")
return
}
response.Success(c, imageGen)
}
func (h *ImageGenerationHandler) ListImageGenerations(c *gin.Context) {
var sceneID *uint
if sceneIDStr := c.Query("scene_id"); sceneIDStr != "" {
id, err := strconv.ParseUint(sceneIDStr, 10, 32)
if err == nil {
uid := uint(id)
sceneID = &uid
}
}
var storyboardID *uint
if storyboardIDStr := c.Query("storyboard_id"); storyboardIDStr != "" {
id, err := strconv.ParseUint(storyboardIDStr, 10, 32)
if err == nil {
uid := uint(id)
storyboardID = &uid
}
}
frameType := c.Query("frame_type")
status := c.Query("status")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
var dramaIDUint *uint
if dramaIDStr := c.Query("drama_id"); dramaIDStr != "" {
did, _ := strconv.ParseUint(dramaIDStr, 10, 32)
didUint := uint(did)
dramaIDUint = &didUint
}
images, total, err := h.imageService.ListImageGenerations(dramaIDUint, sceneID, storyboardID, frameType, status, page, pageSize)
if err != nil {
h.log.Errorw("Failed to list images", "error", err)
response.InternalError(c, err.Error())
return
}
response.SuccessWithPagination(c, images, total, page, pageSize)
}
func (h *ImageGenerationHandler) DeleteImageGeneration(c *gin.Context) {
imageGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
if err := h.imageService.DeleteImageGeneration(uint(imageGenID)); err != nil {
h.log.Errorw("Failed to delete image", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
// UploadImage 上传图片并创建图片生成记录
func (h *ImageGenerationHandler) UploadImage(c *gin.Context) {
var req struct {
StoryboardID uint `json:"storyboard_id" binding:"required"`
DramaID uint `json:"drama_id" binding:"required"`
FrameType string `json:"frame_type" binding:"required"`
ImageURL string `json:"image_url" binding:"required"`
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
imageGen, err := h.imageService.CreateImageFromUpload(&services.UploadImageRequest{
StoryboardID: req.StoryboardID,
DramaID: req.DramaID,
FrameType: req.FrameType,
ImageURL: req.ImageURL,
Prompt: req.Prompt,
})
if err != nil {
h.log.Errorw("Failed to create image from upload", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, imageGen)
}
================================================
FILE: api/handlers/prop.go
================================================
package handlers
import (
"strconv"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type PropHandler struct {
propService *services.PropService
log *logger.Logger
}
func NewPropHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, aiService *services.AIService, imageGenerationService *services.ImageGenerationService) *PropHandler {
return &PropHandler{
propService: services.NewPropService(db, aiService, services.NewTaskService(db, log), imageGenerationService, log, cfg),
log: log,
}
}
// ListProps 获取道具列表
func (h *PropHandler) ListProps(c *gin.Context) {
dramaIDStr := c.Query("drama_id")
if dramaIDStr == "" {
response.BadRequest(c, "drama_id is required")
return
}
dramaID, err := strconv.ParseUint(dramaIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid drama_id")
return
}
props, err := h.propService.ListProps(uint(dramaID))
if err != nil {
response.InternalError(c, err.Error())
return
}
response.Success(c, props)
}
// CreateProp 创建道具
func (h *PropHandler) CreateProp(c *gin.Context) {
var prop models.Prop
if err := c.ShouldBindJSON(&prop); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.propService.CreateProp(&prop); err != nil {
response.InternalError(c, err.Error())
return
}
response.Created(c, prop)
}
// UpdateProp 更新道具
func (h *PropHandler) UpdateProp(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid ID")
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.propService.UpdateProp(uint(id), updates); err != nil {
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
// DeleteProp 删除道具
func (h *PropHandler) DeleteProp(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid ID")
return
}
if err := h.propService.DeleteProp(uint(id)); err != nil {
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
// ExtractProps 提取道具
func (h *PropHandler) ExtractProps(c *gin.Context) {
episodeIDStr := c.Param("episode_id")
episodeID, err := strconv.ParseUint(episodeIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid episode_id")
return
}
taskID, err := h.propService.ExtractPropsFromScript(uint(episodeID))
if err != nil {
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"task_id": taskID})
}
// GenerateImage 生成道具图片
func (h *PropHandler) GenerateImage(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid ID")
return
}
taskID, err := h.propService.GeneratePropImage(uint(id))
if err != nil {
h.log.Errorw("Failed to generate prop image", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"task_id": taskID, "message": "图片生成任务已提交"})
}
// AssociateProps 关联道具
func (h *PropHandler) AssociateProps(c *gin.Context) {
storyboardIDStr := c.Param("id")
storyboardID, err := strconv.ParseUint(storyboardIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid storyboard_id")
return
}
var req struct {
PropIDs []uint `json:"prop_ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.propService.AssociatePropsWithStoryboard(uint(storyboardID), req.PropIDs); err != nil {
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
================================================
FILE: api/handlers/scene.go
================================================
package handlers
import (
services2 "github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type SceneHandler struct {
sceneService *services2.StoryboardCompositionService
log *logger.Logger
}
func NewSceneHandler(db *gorm.DB, log *logger.Logger, imageGenService *services2.ImageGenerationService) *SceneHandler {
return &SceneHandler{
sceneService: services2.NewStoryboardCompositionService(db, log, imageGenService),
log: log,
}
}
func (h *SceneHandler) GetStoryboardsForEpisode(c *gin.Context) {
episodeID := c.Param("episode_id")
storyboards, err := h.sceneService.GetScenesForEpisode(episodeID)
if err != nil {
h.log.Errorw("Failed to get storyboards for episode", "error", err, "episode_id", episodeID)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{
"storyboards": storyboards,
"total": len(storyboards),
})
}
func (h *SceneHandler) UpdateScene(c *gin.Context) {
sceneID := c.Param("scene_id")
var req services2.UpdateSceneInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request")
return
}
if err := h.sceneService.UpdateSceneInfo(sceneID, &req); err != nil {
h.log.Errorw("Failed to update scene", "error", err, "scene_id", sceneID)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"message": "Scene updated successfully"})
}
func (h *SceneHandler) GenerateSceneImage(c *gin.Context) {
var req services2.GenerateSceneImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request")
return
}
imageGen, err := h.sceneService.GenerateSceneImage(&req)
if err != nil {
h.log.Errorw("Failed to generate scene image", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{
"message": "Scene image generation started",
"image_generation": imageGen,
})
}
func (h *SceneHandler) UpdateScenePrompt(c *gin.Context) {
sceneID := c.Param("scene_id")
var req services2.UpdateScenePromptRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request")
return
}
if err := h.sceneService.UpdateScenePrompt(sceneID, &req); err != nil {
h.log.Errorw("Failed to update scene prompt", "error", err, "scene_id", sceneID)
if err.Error() == "scene not found" {
response.NotFound(c, "场景不存在")
return
}
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"message": "场景提示词已更新"})
}
func (h *SceneHandler) DeleteScene(c *gin.Context) {
sceneID := c.Param("scene_id")
if err := h.sceneService.DeleteScene(sceneID); err != nil {
h.log.Errorw("Failed to delete scene", "error", err, "scene_id", sceneID)
if err.Error() == "scene not found" {
response.NotFound(c, "场景不存在")
return
}
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"message": "场景已删除"})
}
func (h *SceneHandler) CreateScene(c *gin.Context) {
var req services2.CreateSceneRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request")
return
}
if req.DramaID == 0 {
response.BadRequest(c, "drama_id is required")
return
}
scene, err := h.sceneService.CreateScene(&req)
if err != nil {
h.log.Errorw("Failed to create scene", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, scene)
}
================================================
FILE: api/handlers/script_generation.go
================================================
package handlers
import (
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ScriptGenerationHandler struct {
scriptService *services.ScriptGenerationService
taskService *services.TaskService
log *logger.Logger
}
func NewScriptGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *ScriptGenerationHandler {
return &ScriptGenerationHandler{
scriptService: services.NewScriptGenerationService(db, cfg, log),
taskService: services.NewTaskService(db, log),
log: log,
}
}
func (h *ScriptGenerationHandler) GenerateCharacters(c *gin.Context) {
var req services.GenerateCharactersRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// 直接调用服务层的异步方法,该方法会创建任务并返回任务ID
taskID, err := h.scriptService.GenerateCharacters(&req)
if err != nil {
h.log.Errorw("Failed to generate characters", "error", err, "drama_id", req.DramaID)
response.InternalError(c, err.Error())
return
}
// 立即返回任务ID
response.Success(c, gin.H{
"task_id": taskID,
"status": "pending",
"message": "角色生成任务已创建,正在后台处理...",
})
}
================================================
FILE: api/handlers/settings.go
================================================
package handlers
import (
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
type SettingsHandler struct {
config *config.Config
log *logger.Logger
}
func NewSettingsHandler(cfg *config.Config, log *logger.Logger) *SettingsHandler {
return &SettingsHandler{
config: cfg,
log: log,
}
}
// GetLanguage 获取当前系统语言
func (h *SettingsHandler) GetLanguage(c *gin.Context) {
language := h.config.App.Language
if language == "" {
language = "zh" // 默认中文
}
response.Success(c, gin.H{
"language": language,
})
}
// UpdateLanguage 更新系统语言
func (h *SettingsHandler) UpdateLanguage(c *gin.Context) {
var req struct {
Language string `json:"language" binding:"required,oneof=zh en"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "语言参数错误,只支持 zh 或 en")
return
}
// 更新内存中的配置
h.config.App.Language = req.Language
// 更新配置文件
viper.Set("app.language", req.Language)
if err := viper.WriteConfig(); err != nil {
h.log.Warnw("Failed to write config file", "error", err)
// 即使写入文件失败,内存配置也已更新,仍然可用
}
h.log.Infow("System language updated", "language", req.Language)
message := "语言已切换为中文"
if req.Language == "en" {
message = "Language switched to English"
}
response.Success(c, gin.H{
"message": message,
"language": req.Language,
})
}
================================================
FILE: api/handlers/storyboard.go
================================================
package handlers
import (
"strconv"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type StoryboardHandler struct {
storyboardService *services.StoryboardService
taskService *services.TaskService
log *logger.Logger
}
func NewStoryboardHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *StoryboardHandler {
return &StoryboardHandler{
storyboardService: services.NewStoryboardService(db, cfg, log),
taskService: services.NewTaskService(db, log),
log: log,
}
}
// GenerateStoryboard 生成分镜头(异步)
func (h *StoryboardHandler) GenerateStoryboard(c *gin.Context) {
episodeID := c.Param("episode_id")
// 接收可选的 model 参数
var req struct {
Model string `json:"model"`
}
if err := c.ShouldBindJSON(&req); err != nil {
// 如果没有提供body或者解析失败,使用空字符串(使用默认模型)
req.Model = ""
}
// 调用生成服务,该服务已经是异步的,会返回任务ID
taskID, err := h.storyboardService.GenerateStoryboard(episodeID, req.Model)
if err != nil {
h.log.Errorw("Failed to generate storyboard", "error", err, "episode_id", episodeID)
response.InternalError(c, err.Error())
return
}
// 立即返回任务ID
response.Success(c, gin.H{
"task_id": taskID,
"status": "pending",
"message": "分镜头生成任务已创建,正在后台处理...",
})
}
// UpdateStoryboard 更新分镜
func (h *StoryboardHandler) UpdateStoryboard(c *gin.Context) {
storyboardID := c.Param("id")
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request body")
return
}
err := h.storyboardService.UpdateStoryboard(storyboardID, req)
if err != nil {
h.log.Errorw("Failed to update storyboard", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"message": "Storyboard updated successfully"})
}
// CreateStoryboard 创建分镜
func (h *StoryboardHandler) CreateStoryboard(c *gin.Context) {
var req services.CreateStoryboardRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
sb, err := h.storyboardService.CreateStoryboard(&req)
if err != nil {
h.log.Errorw("Failed to create storyboard", "error", err)
response.InternalError(c, err.Error())
return
}
response.Created(c, sb)
}
// DeleteStoryboard 删除分镜
func (h *StoryboardHandler) DeleteStoryboard(c *gin.Context) {
storyboardIDStr := c.Param("id")
storyboardID, err := strconv.ParseUint(storyboardIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid ID")
return
}
if err := h.storyboardService.DeleteStoryboard(uint(storyboardID)); err != nil {
h.log.Errorw("Failed to delete storyboard", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
================================================
FILE: api/handlers/task.go
================================================
package handlers
import (
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type TaskHandler struct {
taskService *services.TaskService
log *logger.Logger
}
func NewTaskHandler(db *gorm.DB, log *logger.Logger) *TaskHandler {
return &TaskHandler{
taskService: services.NewTaskService(db, log),
log: log,
}
}
// GetTaskStatus 获取任务状态
func (h *TaskHandler) GetTaskStatus(c *gin.Context) {
taskID := c.Param("task_id")
task, err := h.taskService.GetTask(taskID)
if err != nil {
if err == gorm.ErrRecordNotFound {
response.NotFound(c, "任务不存在")
return
}
h.log.Errorw("Failed to get task", "error", err, "task_id", taskID)
response.InternalError(c, err.Error())
return
}
response.Success(c, task)
}
// GetResourceTasks 获取资源相关的所有任务
func (h *TaskHandler) GetResourceTasks(c *gin.Context) {
resourceID := c.Query("resource_id")
if resourceID == "" {
response.BadRequest(c, "缺少resource_id参数")
return
}
tasks, err := h.taskService.GetTasksByResource(resourceID)
if err != nil {
h.log.Errorw("Failed to get resource tasks", "error", err, "resource_id", resourceID)
response.InternalError(c, err.Error())
return
}
response.Success(c, tasks)
}
================================================
FILE: api/handlers/upload.go
================================================
package handlers
import (
services2 "github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type UploadHandler struct {
uploadService *services2.UploadService
characterLibraryService *services2.CharacterLibraryService
log *logger.Logger
}
func NewUploadHandler(cfg *config.Config, log *logger.Logger, characterLibraryService *services2.CharacterLibraryService) (*UploadHandler, error) {
uploadService, err := services2.NewUploadService(cfg, log)
if err != nil {
return nil, err
}
return &UploadHandler{
uploadService: uploadService,
characterLibraryService: characterLibraryService,
log: log,
}, nil
}
// UploadImage 上传图片
func (h *UploadHandler) UploadImage(c *gin.Context) {
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
response.BadRequest(c, "请选择文件")
return
}
defer file.Close()
// 检查文件类型
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
// 验证是图片类型
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/jpg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
}
if !allowedTypes[contentType] {
response.BadRequest(c, "只支持图片格式 (jpg, png, gif, webp)")
return
}
// 检查文件大小 (10MB)
if header.Size > 10*1024*1024 {
response.BadRequest(c, "文件大小不能超过10MB")
return
}
// 上传到本地存储
result, err := h.uploadService.UploadCharacterImage(file, header.Filename, contentType)
if err != nil {
h.log.Errorw("Failed to upload image", "error", err)
response.InternalError(c, "上传失败")
return
}
response.Success(c, gin.H{
"url": result.URL,
"local_path": result.LocalPath,
"filename": header.Filename,
"size": header.Size,
})
}
// UploadCharacterImage 上传角色图片(带角色ID)
func (h *UploadHandler) UploadCharacterImage(c *gin.Context) {
characterID := c.Param("id")
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
response.BadRequest(c, "请选择文件")
return
}
defer file.Close()
// 检查文件类型
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
// 验证是图片类型
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/jpg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
}
if !allowedTypes[contentType] {
response.BadRequest(c, "只支持图片格式 (jpg, png, gif, webp)")
return
}
// 检查文件大小 (10MB)
if header.Size > 10*1024*1024 {
response.BadRequest(c, "文件大小不能超过10MB")
return
}
// 上传到本地存储
result, err := h.uploadService.UploadCharacterImage(file, header.Filename, contentType)
if err != nil {
h.log.Errorw("Failed to upload character image", "error", err)
response.InternalError(c, "上传失败")
return
}
// 更新角色的image_url字段到数据库
err = h.characterLibraryService.UploadCharacterImage(characterID, result.URL)
if err != nil {
h.log.Errorw("Failed to update character image_url", "error", err, "character_id", characterID)
response.InternalError(c, "更新角色图片失败")
return
}
h.log.Infow("Character image uploaded and saved", "character_id", characterID, "url", result.URL, "local_path", result.LocalPath)
response.Success(c, gin.H{
"url": result.URL,
"local_path": result.LocalPath,
"filename": header.Filename,
"size": header.Size,
})
}
================================================
FILE: api/handlers/video_generation.go
================================================
package handlers
import (
"strconv"
"github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/infrastructure/storage"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type VideoGenerationHandler struct {
videoService *services.VideoGenerationService
log *logger.Logger
}
func NewVideoGenerationHandler(db *gorm.DB, transferService *services.ResourceTransferService, localStorage *storage.LocalStorage, aiService *services.AIService, log *logger.Logger, promptI18n *services.PromptI18n) *VideoGenerationHandler {
return &VideoGenerationHandler{
videoService: services.NewVideoGenerationService(db, transferService, localStorage, aiService, log, promptI18n),
log: log,
}
}
func (h *VideoGenerationHandler) GenerateVideo(c *gin.Context) {
var req services.GenerateVideoRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
videoGen, err := h.videoService.GenerateVideo(&req)
if err != nil {
h.log.Errorw("Failed to generate video", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, videoGen)
}
func (h *VideoGenerationHandler) GenerateVideoFromImage(c *gin.Context) {
imageGenID, err := strconv.ParseUint(c.Param("image_gen_id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的图片ID")
return
}
videoGen, err := h.videoService.GenerateVideoFromImage(uint(imageGenID))
if err != nil {
h.log.Errorw("Failed to generate video from image", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, videoGen)
}
func (h *VideoGenerationHandler) BatchGenerateForEpisode(c *gin.Context) {
episodeID := c.Param("episode_id")
videos, err := h.videoService.BatchGenerateVideosForEpisode(episodeID)
if err != nil {
h.log.Errorw("Failed to batch generate videos", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, videos)
}
func (h *VideoGenerationHandler) GetVideoGeneration(c *gin.Context) {
videoGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
videoGen, err := h.videoService.GetVideoGeneration(uint(videoGenID))
if err != nil {
response.NotFound(c, "视频生成记录不存在")
return
}
response.Success(c, videoGen)
}
func (h *VideoGenerationHandler) ListVideoGenerations(c *gin.Context) {
var storyboardID *uint
// 优先使用storyboard_id参数
if storyboardIDStr := c.Query("storyboard_id"); storyboardIDStr != "" {
id, err := strconv.ParseUint(storyboardIDStr, 10, 32)
if err == nil {
uid := uint(id)
storyboardID = &uid
}
}
status := c.Query("status")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
var dramaIDUint *uint
if dramaIDStr := c.Query("drama_id"); dramaIDStr != "" {
did, _ := strconv.ParseUint(dramaIDStr, 10, 32)
didUint := uint(did)
dramaIDUint = &didUint
}
// 计算offset:(page - 1) * pageSize
offset := (page - 1) * pageSize
videos, total, err := h.videoService.ListVideoGenerations(dramaIDUint, storyboardID, status, pageSize, offset)
if err != nil {
h.log.Errorw("Failed to list videos", "error", err)
response.InternalError(c, err.Error())
return
}
response.SuccessWithPagination(c, videos, total, page, pageSize)
}
func (h *VideoGenerationHandler) DeleteVideoGeneration(c *gin.Context) {
videoGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
response.BadRequest(c, "无效的ID")
return
}
if err := h.videoService.DeleteVideoGeneration(uint(videoGenID)); err != nil {
h.log.Errorw("Failed to delete video", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, nil)
}
================================================
FILE: api/handlers/video_merge.go
================================================
package handlers
import (
"strconv"
services2 "github.com/drama-generator/backend/application/services"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type VideoMergeHandler struct {
mergeService *services2.VideoMergeService
log *logger.Logger
}
func NewVideoMergeHandler(db *gorm.DB, transferService *services2.ResourceTransferService, storagePath, baseURL string, log *logger.Logger) *VideoMergeHandler {
return &VideoMergeHandler{
mergeService: services2.NewVideoMergeService(db, transferService, storagePath, baseURL, log),
log: log,
}
}
func (h *VideoMergeHandler) MergeVideos(c *gin.Context) {
var req services2.MergeVideoRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request")
return
}
merge, err := h.mergeService.MergeVideos(&req)
if err != nil {
h.log.Errorw("Failed to merge videos", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{
"message": "Video merge task created",
"merge": merge,
})
}
func (h *VideoMergeHandler) GetMerge(c *gin.Context) {
mergeIDStr := c.Param("merge_id")
mergeID, err := strconv.ParseUint(mergeIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid merge ID")
return
}
merge, err := h.mergeService.GetMerge(uint(mergeID))
if err != nil {
h.log.Errorw("Failed to get merge", "error", err)
response.NotFound(c, "Merge not found")
return
}
response.Success(c, gin.H{"merge": merge})
}
func (h *VideoMergeHandler) ListMerges(c *gin.Context) {
episodeID := c.Query("episode_id")
status := c.Query("status")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var episodeIDPtr *string
if episodeID != "" {
episodeIDPtr = &episodeID
}
merges, total, err := h.mergeService.ListMerges(episodeIDPtr, status, page, pageSize)
if err != nil {
h.log.Errorw("Failed to list merges", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{
"merges": merges,
"total": total,
"page": page,
"page_size": pageSize,
})
}
func (h *VideoMergeHandler) DeleteMerge(c *gin.Context) {
mergeIDStr := c.Param("merge_id")
mergeID, err := strconv.ParseUint(mergeIDStr, 10, 32)
if err != nil {
response.BadRequest(c, "Invalid merge ID")
return
}
if err := h.mergeService.DeleteMerge(uint(mergeID)); err != nil {
h.log.Errorw("Failed to delete merge", "error", err)
response.InternalError(c, err.Error())
return
}
response.Success(c, gin.H{"message": "Merge deleted successfully"})
}
================================================
FILE: api/middlewares/cors.go
================================================
package middlewares
import (
"github.com/gin-gonic/gin"
)
func CORSMiddleware(allowedOrigins []string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
path := c.Request.URL.Path
// 检查是否是静态文件路径(/static 或 /assets)
isStaticPath := len(path) >= 7 && (path[:7] == "/static" || path[:7] == "/assets")
allowed := false
for _, o := range allowedOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
// 对于静态文件,如果有 Origin 头,总是允许跨域访问
if isStaticPath && origin != "" {
allowed = true
}
if allowed && origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
} else if allowed && origin == "" {
// 如果没有 Origin 头但是允许的请求,设置为 *
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
}
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type, Content-Disposition")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
================================================
FILE: api/middlewares/logger.go
================================================
package middlewares
import (
"time"
"github.com/drama-generator/backend/pkg/logger"
"github.com/gin-gonic/gin"
)
func LoggerMiddleware(log *logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
duration := time.Since(start)
log.Infow("HTTP Request",
"method", c.Request.Method,
"path", path,
"query", query,
"status", c.Writer.Status(),
"duration", duration.Milliseconds(),
"ip", c.ClientIP(),
"user_agent", c.Request.UserAgent(),
)
}
}
================================================
FILE: api/middlewares/ratelimit.go
================================================
package middlewares
import (
"sync"
"time"
"github.com/drama-generator/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type rateLimiter struct {
mu sync.Mutex
requests map[string][]time.Time
limit int
window time.Duration
}
var limiter = &rateLimiter{
requests: make(map[string][]time.Time),
limit: 2000, // 每分钟最多 2000 次请求
window: time.Minute,
}
func RateLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
limiter.mu.Lock()
defer limiter.mu.Unlock()
now := time.Now()
requests := limiter.requests[ip]
var validRequests []time.Time
for _, t := range requests {
if now.Sub(t) < limiter.window {
validRequests = append(validRequests, t)
}
}
if len(validRequests) >= limiter.limit {
response.Error(c, 429, "RATE_LIMIT_EXCEEDED", "请求过于频繁,请稍后再试")
c.Abort()
return
}
validRequests = append(validRequests, now)
limiter.requests[ip] = validRequests
c.Next()
}
}
================================================
FILE: api/routes/routes.go
================================================
package routes
import (
handlers2 "github.com/drama-generator/backend/api/handlers"
middlewares2 "github.com/drama-generator/backend/api/middlewares"
services2 "github.com/drama-generator/backend/application/services"
storage2 "github.com/drama-generator/backend/infrastructure/storage"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func SetupRouter(cfg *config.Config, db *gorm.DB, log *logger.Logger, localStorage interface{}) *gin.Engine {
r := gin.New()
r.Use(gin.Recovery())
r.Use(middlewares2.LoggerMiddleware(log))
r.Use(middlewares2.CORSMiddleware(cfg.Server.CORSOrigins))
// 静态文件服务(用户上传的文件)
r.Static("/static", cfg.Storage.LocalPath)
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"app": cfg.App.Name,
"version": cfg.App.Version,
})
})
aiService := services2.NewAIService(db, log)
localStoragePtr := localStorage.(*storage2.LocalStorage)
transferService := services2.NewResourceTransferService(db, log)
promptI18n := services2.NewPromptI18n(cfg)
dramaHandler := handlers2.NewDramaHandler(db, cfg, log, nil)
aiConfigHandler := handlers2.NewAIConfigHandler(db, cfg, log)
scriptGenHandler := handlers2.NewScriptGenerationHandler(db, cfg, log)
imageGenService := services2.NewImageGenerationService(db, cfg, transferService, localStoragePtr, log)
imageGenHandler := handlers2.NewImageGenerationHandler(db, cfg, log, transferService, localStoragePtr)
videoGenHandler := handlers2.NewVideoGenerationHandler(db, transferService, localStoragePtr, aiService, log, promptI18n)
videoMergeHandler := handlers2.NewVideoMergeHandler(db, nil, cfg.Storage.LocalPath, cfg.Storage.BaseURL, log)
assetHandler := handlers2.NewAssetHandler(db, cfg, log)
characterLibraryService := services2.NewCharacterLibraryService(db, log, cfg)
characterLibraryHandler := handlers2.NewCharacterLibraryHandler(db, cfg, log, transferService, localStoragePtr)
uploadHandler, err := handlers2.NewUploadHandler(cfg, log, characterLibraryService)
if err != nil {
log.Fatalw("Failed to create upload handler", "error", err)
}
storyboardHandler := handlers2.NewStoryboardHandler(db, cfg, log)
sceneHandler := handlers2.NewSceneHandler(db, log, imageGenService)
taskHandler := handlers2.NewTaskHandler(db, log)
framePromptService := services2.NewFramePromptService(db, cfg, log)
framePromptHandler := handlers2.NewFramePromptHandler(framePromptService, log)
audioExtractionHandler := handlers2.NewAudioExtractionHandler(log, cfg.Storage.LocalPath)
settingsHandler := handlers2.NewSettingsHandler(cfg, log)
propHandler := handlers2.NewPropHandler(db, cfg, log, aiService, imageGenService)
api := r.Group("/api/v1")
{
api.Use(middlewares2.RateLimitMiddleware())
dramas := api.Group("/dramas")
{
dramas.GET("", dramaHandler.ListDramas)
dramas.POST("", dramaHandler.CreateDrama)
dramas.GET("/stats", dramaHandler.GetDramaStats) // 统计接口放在/:id之前
dramas.GET("/:id", dramaHandler.GetDrama)
dramas.PUT("/:id", dramaHandler.UpdateDrama)
dramas.DELETE("/:id", dramaHandler.DeleteDrama)
dramas.PUT("/:id/outline", dramaHandler.SaveOutline)
dramas.GET("/:id/characters", dramaHandler.GetCharacters)
dramas.PUT("/:id/characters", dramaHandler.SaveCharacters)
dramas.PUT("/:id/episodes", dramaHandler.SaveEpisodes)
dramas.PUT("/:id/progress", dramaHandler.SaveProgress)
dramas.GET("/:id/props", propHandler.ListProps) // Added prop list route
}
aiConfigs := api.Group("/ai-configs")
{
aiConfigs.GET("", aiConfigHandler.ListConfigs)
aiConfigs.POST("", aiConfigHandler.CreateConfig)
aiConfigs.POST("/test", aiConfigHandler.TestConnection)
aiConfigs.GET("/:id", aiConfigHandler.GetConfig)
aiConfigs.PUT("/:id", aiConfigHandler.UpdateConfig)
aiConfigs.DELETE("/:id", aiConfigHandler.DeleteConfig)
}
generation := api.Group("/generation")
{
generation.POST("/characters", scriptGenHandler.GenerateCharacters)
}
// 角色库路由
characterLibrary := api.Group("/character-library")
{
characterLibrary.GET("", characterLibraryHandler.ListLibraryItems)
characterLibrary.POST("", characterLibraryHandler.CreateLibraryItem)
characterLibrary.GET("/:id", characterLibraryHandler.GetLibraryItem)
characterLibrary.DELETE("/:id", characterLibraryHandler.DeleteLibraryItem)
}
// 角色图片相关路由
characters := api.Group("/characters")
{
characters.PUT("/:id", characterLibraryHandler.UpdateCharacter)
characters.DELETE("/:id", characterLibraryHandler.DeleteCharacter)
characters.POST("/batch-generate-images", characterLibraryHandler.BatchGenerateCharacterImages)
characters.POST("/:id/generate-image", characterLibraryHandler.GenerateCharacterImage)
characters.POST("/:id/upload-image", uploadHandler.UploadCharacterImage)
characters.PUT("/:id/image", characterLibraryHandler.UploadCharacterImage)
characters.PUT("/:id/image-from-library", characterLibraryHandler.ApplyLibraryItemToCharacter)
characters.POST("/:id/add-to-library", characterLibraryHandler.AddCharacterToLibrary)
}
props := api.Group("/props")
{
props.POST("", propHandler.CreateProp)
props.PUT("/:id", propHandler.UpdateProp)
props.DELETE("/:id", propHandler.DeleteProp)
props.POST("/:id/generate", propHandler.GenerateImage)
}
// 文件上传路由
upload := api.Group("/upload")
{
upload.POST("/image", uploadHandler.UploadImage)
}
// 分镜头路由
episodes := api.Group("/episodes")
{
// 分镜头
episodes.POST("/:episode_id/storyboards", storyboardHandler.GenerateStoryboard)
episodes.POST("/:episode_id/props/extract", propHandler.ExtractProps)
episodes.POST("/:episode_id/characters/extract", characterLibraryHandler.ExtractCharacters)
episodes.GET("/:episode_id/storyboards", sceneHandler.GetStoryboardsForEpisode)
episodes.POST("/:episode_id/finalize", dramaHandler.FinalizeEpisode)
episodes.GET("/:episode_id/download", dramaHandler.DownloadEpisodeVideo)
}
// 任务路由
tasks := api.Group("/tasks")
{
tasks.GET("/:task_id", taskHandler.GetTaskStatus)
tasks.GET("", taskHandler.GetResourceTasks)
}
// 场景路由
scenes := api.Group("/scenes")
{
scenes.PUT("/:scene_id", sceneHandler.UpdateScene)
scenes.PUT("/:scene_id/prompt", sceneHandler.UpdateScenePrompt)
scenes.DELETE("/:scene_id", sceneHandler.DeleteScene)
scenes.POST("/generate-image", sceneHandler.GenerateSceneImage)
scenes.POST("", sceneHandler.CreateScene)
}
images := api.Group("/images")
{
images.GET("", imageGenHandler.ListImageGenerations)
images.POST("", imageGenHandler.GenerateImage)
images.GET("/:id", imageGenHandler.GetImageGeneration)
images.DELETE("/:id", imageGenHandler.DeleteImageGeneration)
images.POST("/scene/:scene_id", imageGenHandler.GenerateImagesForScene)
images.POST("/upload", imageGenHandler.UploadImage)
images.GET("/episode/:episode_id/backgrounds", imageGenHandler.GetBackgroundsForEpisode)
images.POST("/episode/:episode_id/backgrounds/extract", imageGenHandler.ExtractBackgroundsForEpisode)
images.POST("/episode/:episode_id/batch", imageGenHandler.BatchGenerateForEpisode)
}
videos := api.Group("/videos")
{
videos.GET("", videoGenHandler.ListVideoGenerations)
videos.POST("", videoGenHandler.GenerateVideo)
videos.GET("/:id", videoGenHandler.GetVideoGeneration)
videos.DELETE("/:id", videoGenHandler.DeleteVideoGeneration)
videos.POST("/image/:image_gen_id", videoGenHandler.GenerateVideoFromImage)
videos.POST("/episode/:episode_id/batch", videoGenHandler.BatchGenerateForEpisode)
}
videoMerges := api.Group("/video-merges")
{
videoMerges.GET("", videoMergeHandler.ListMerges)
videoMerges.POST("", videoMergeHandler.MergeVideos)
videoMerges.GET("/:merge_id", videoMergeHandler.GetMerge)
videoMerges.DELETE("/:merge_id", videoMergeHandler.DeleteMerge)
}
assets := api.Group("/assets")
{
assets.GET("", assetHandler.ListAssets)
assets.POST("", assetHandler.CreateAsset)
assets.GET("/:id", assetHandler.GetAsset)
assets.PUT("/:id", assetHandler.UpdateAsset)
assets.DELETE("/:id", assetHandler.DeleteAsset)
assets.POST("/import/image/:image_gen_id", assetHandler.ImportFromImageGen)
assets.POST("/import/video/:video_gen_id", assetHandler.ImportFromVideoGen)
}
storyboards := api.Group("/storyboards")
{
storyboards.GET("/episode/:episode_id/generate", storyboardHandler.GenerateStoryboard)
storyboards.POST("", storyboardHandler.CreateStoryboard)
storyboards.PUT("/:id", storyboardHandler.UpdateStoryboard)
storyboards.DELETE("/:id", storyboardHandler.DeleteStoryboard)
storyboards.POST("/:id/props", propHandler.AssociateProps)
storyboards.POST("/:id/frame-prompt", framePromptHandler.GenerateFramePrompt)
storyboards.GET("/:id/frame-prompts", handlers2.GetStoryboardFramePrompts(db, log))
}
audio := api.Group("/audio")
{
audio.POST("/extract", audioExtractionHandler.ExtractAudio)
audio.POST("/extract/batch", audioExtractionHandler.BatchExtractAudio)
}
settings := api.Group("/settings")
{
settings.GET("/language", settingsHandler.GetLanguage)
settings.PUT("/language", settingsHandler.UpdateLanguage)
}
}
// 前端静态文件服务(放在API路由之后,避免冲突)
// 服务前端构建产物
r.Static("/assets", "./web/dist/assets")
r.StaticFile("/favicon.ico", "./web/dist/favicon.ico")
// NoRoute处理:对于所有未匹配的路由
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
// 如果是API路径,返回404
if len(path) >= 4 && path[:4] == "/api" {
c.JSON(404, gin.H{"error": "API endpoint not found"})
return
}
// SPA fallback - 返回index.html
c.File("./web/dist/index.html")
})
return r
}
================================================
FILE: application/services/ai_service.go
================================================
package services
import (
"errors"
"fmt"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/ai"
"github.com/drama-generator/backend/pkg/logger"
"gorm.io/gorm"
)
type AIService struct {
db *gorm.DB
log *logger.Logger
}
func NewAIService(db *gorm.DB, log *logger.Logger) *AIService {
return &AIService{
db: db,
log: log,
}
}
type CreateAIConfigRequest struct {
ServiceType string `json:"service_type" binding:"required,oneof=text image video"`
Name string `json:"name" binding:"required,min=1,max=100"`
Provider string `json:"provider" binding:"required"`
BaseURL string `json:"base_url" binding:"required,url"`
APIKey string `json:"api_key" binding:"required"`
Model models.ModelField `json:"model" binding:"required"`
Endpoint string `json:"endpoint"`
QueryEndpoint string `json:"query_endpoint"`
Priority int `json:"priority"`
IsDefault bool `json:"is_default"`
Settings string `json:"settings"`
}
type UpdateAIConfigRequest struct {
Name string `json:"name" binding:"omitempty,min=1,max=100"`
Provider string `json:"provider"`
BaseURL string `json:"base_url" binding:"omitempty,url"`
APIKey string `json:"api_key"`
Model *models.ModelField `json:"model"`
Endpoint string `json:"endpoint"`
QueryEndpoint string `json:"query_endpoint"`
Priority *int `json:"priority"`
IsDefault bool `json:"is_default"`
IsActive bool `json:"is_active"`
Settings string `json:"settings"`
}
type TestConnectionRequest struct {
BaseURL string `json:"base_url" binding:"required,url"`
APIKey string `json:"api_key" binding:"required"`
Model models.ModelField `json:"model" binding:"required"`
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
}
func (s *AIService) CreateConfig(req *CreateAIConfigRequest) (*models.AIServiceConfig, error) {
// 根据 provider 和 service_type 自动设置 endpoint
endpoint := req.Endpoint
queryEndpoint := req.QueryEndpoint
if endpoint == "" {
switch req.Provider {
case "gemini", "google":
if req.ServiceType == "text" {
endpoint = "/v1beta/models/{model}:generateContent"
} else if req.ServiceType == "image" {
endpoint = "/v1beta/models/{model}:generateContent"
}
case "openai":
if req.ServiceType == "text" {
endpoint = "/chat/completions"
} else if req.ServiceType == "image" {
endpoint = "/images/generations"
} else if req.ServiceType == "video" {
endpoint = "/videos"
if queryEndpoint == "" {
queryEndpoint = "/videos/{taskId}"
}
}
case "chatfire":
if req.ServiceType == "text" {
endpoint = "/chat/completions"
} else if req.ServiceType == "image" {
endpoint = "/images/generations"
} else if req.ServiceType == "video" {
endpoint = "/video/generations"
if queryEndpoint == "" {
queryEndpoint = "/video/task/{taskId}"
}
}
case "doubao", "volcengine", "volces":
if req.ServiceType == "video" {
endpoint = "/contents/generations/tasks"
if queryEndpoint == "" {
queryEndpoint = "/generations/tasks/{taskId}"
}
}
default:
// 默认使用 OpenAI 格式
if req.ServiceType == "text" {
endpoint = "/chat/completions"
} else if req.ServiceType == "image" {
endpoint = "/images/generations"
}
}
}
config := &models.AIServiceConfig{
ServiceType: req.ServiceType,
Name: req.Name,
Provider: req.Provider,
BaseURL: req.BaseURL,
APIKey: req.APIKey,
Model: req.Model,
Endpoint: endpoint,
QueryEndpoint: queryEndpoint,
Priority: req.Priority,
IsDefault: req.IsDefault,
IsActive: true,
Settings: req.Settings,
}
if err := s.db.Create(config).Error; err != nil {
s.log.Errorw("Failed to create AI config", "error", err)
return nil, err
}
s.log.Infow("AI config created", "config_id", config.ID, "provider", req.Provider, "endpoint", endpoint)
return config, nil
}
func (s *AIService) GetConfig(configID uint) (*models.AIServiceConfig, error) {
var config models.AIServiceConfig
err := s.db.Where("id = ? ", configID).First(&config).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("config not found")
}
return nil, err
}
return &config, nil
}
func (s *AIService) ListConfigs(serviceType string) ([]models.AIServiceConfig, error) {
var configs []models.AIServiceConfig
query := s.db
if serviceType != "" {
query = query.Where("service_type = ?", serviceType)
}
err := query.Order("priority DESC, created_at DESC").Find(&configs).Error
if err != nil {
s.log.Errorw("Failed to list AI configs", "error", err)
return nil, err
}
return configs, nil
}
func (s *AIService) UpdateConfig(configID uint, req *UpdateAIConfigRequest) (*models.AIServiceConfig, error) {
var config models.AIServiceConfig
if err := s.db.Where("id = ? ", configID).First(&config).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("config not found")
}
return nil, err
}
tx := s.db.Begin()
// 不再需要is_default独占逻辑
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.Provider != "" {
updates["provider"] = req.Provider
}
if req.BaseURL != "" {
updates["base_url"] = req.BaseURL
}
if req.APIKey != "" {
updates["api_key"] = req.APIKey
}
if req.Model != nil && len(*req.Model) > 0 {
updates["model"] = *req.Model
}
if req.Priority != nil {
updates["priority"] = *req.Priority
}
// 如果提供了 provider,根据 provider 和 service_type 自动设置 endpoint
if req.Provider != "" && req.Endpoint == "" {
provider := req.Provider
serviceType := config.ServiceType
switch provider {
case "gemini", "google":
if serviceType == "text" || serviceType == "image" {
updates["endpoint"] = "/v1beta/models/{model}:generateContent"
}
case "openai":
if serviceType == "text" {
updates["endpoint"] = "/chat/completions"
} else if serviceType == "image" {
updates["endpoint"] = "/images/generations"
} else if serviceType == "video" {
updates["endpoint"] = "/videos"
updates["query_endpoint"] = "/videos/{taskId}"
}
case "chatfire":
if serviceType == "text" {
updates["endpoint"] = "/chat/completions"
} else if serviceType == "image" {
updates["endpoint"] = "/images/generations"
} else if serviceType == "video" {
updates["endpoint"] = "/video/generations"
updates["query_endpoint"] = "/video/task/{taskId}"
}
}
} else if req.Endpoint != "" {
updates["endpoint"] = req.Endpoint
}
// 允许清空query_endpoint,所以不检查是否为空
updates["query_endpoint"] = req.QueryEndpoint
if req.Settings != "" {
updates["settings"] = req.Settings
}
updates["is_default"] = req.IsDefault
updates["is_active"] = req.IsActive
if err := tx.Model(&config).Updates(updates).Error; err != nil {
tx.Rollback()
s.log.Errorw("Failed to update AI config", "error", err)
return nil, err
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
s.log.Infow("AI config updated", "config_id", configID)
return &config, nil
}
func (s *AIService) DeleteConfig(configID uint) error {
result := s.db.Where("id = ? ", configID).Delete(&models.AIServiceConfig{})
if result.Error != nil {
s.log.Errorw("Failed to delete AI config", "error", result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("config not found")
}
s.log.Infow("AI config deleted", "config_id", configID)
return nil
}
func (s *AIService) TestConnection(req *TestConnectionRequest) error {
s.log.Infow("TestConnection called", "baseURL", req.BaseURL, "provider", req.Provider, "endpoint", req.Endpoint, "modelCount", len(req.Model))
// 使用第一个模型进行测试
model := ""
if len(req.Model) > 0 {
model = req.Model[0]
}
s.log.Infow("Using model for test", "model", model, "provider", req.Provider)
// 根据 provider 参数选择客户端
var client ai.AIClient
var endpoint string
switch req.Provider {
case "gemini", "google":
// Gemini
s.log.Infow("Using Gemini client", "baseURL", req.BaseURL)
endpoint = "/v1beta/models/{model}:generateContent"
client = ai.NewGeminiClient(req.BaseURL, req.APIKey, model, endpoint)
case "openai", "chatfire":
// OpenAI 格式(包括 chatfire 等)
s.log.Infow("Using OpenAI-compatible client", "baseURL", req.BaseURL, "provider", req.Provider)
endpoint = req.Endpoint
if endpoint == "" {
endpoint = "/chat/completions"
}
client = ai.NewOpenAIClient(req.BaseURL, req.APIKey, model, endpoint)
default:
// 默认使用 OpenAI 格式
s.log.Infow("Using default OpenAI-compatible client", "baseURL", req.BaseURL)
endpoint = req.Endpoint
if endpoint == "" {
endpoint = "/chat/completions"
}
client = ai.NewOpenAIClient(req.BaseURL, req.APIKey, model, endpoint)
}
s.log.Infow("Calling TestConnection on client", "endpoint", endpoint)
err := client.TestConnection()
if err != nil {
s.log.Errorw("TestConnection failed", "error", err)
} else {
s.log.Infow("TestConnection succeeded")
}
return err
}
func (s *AIService) GetDefaultConfig(serviceType string) (*models.AIServiceConfig, error) {
var config models.AIServiceConfig
// 按优先级降序获取第一个激活的配置
err := s.db.Where("service_type = ? AND is_active = ?", serviceType, true).
Order("priority DESC, created_at DESC").
First(&config).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("no active config found")
}
return nil, err
}
return &config, nil
}
// GetConfigForModel 根据服务类型和模型名称获取优先级最高的激活配置
func (s *AIService) GetConfigForModel(serviceType string, modelName string) (*models.AIServiceConfig, error) {
var configs []models.AIServiceConfig
err := s.db.Where("service_type = ? AND is_active = ?", serviceType, true).
Order("priority DESC, created_at DESC").
Find(&configs).Error
if err != nil {
return nil, err
}
// 查找包含指定模型的配置
for _, config := range configs {
for _, model := range config.Model {
if model == modelName {
return &config, nil
}
}
}
return nil, errors.New("no active config found for model: " + modelName)
}
func (s *AIService) GetAIClient(serviceType string) (ai.AIClient, error) {
config, err := s.GetDefaultConfig(serviceType)
if err != nil {
return nil, err
}
// 使用第一个模型
model := ""
if len(config.Model) > 0 {
model = config.Model[0]
}
// 使用数据库配置中的 endpoint,如果为空则根据 provider 设置默认值
endpoint := config.Endpoint
if endpoint == "" {
switch config.Provider {
case "gemini", "google":
endpoint = "/v1beta/models/{model}:generateContent"
default:
endpoint = "/chat/completions"
}
}
// 根据 provider 创建对应的客户端
switch config.Provider {
case "gemini", "google":
return ai.NewGeminiClient(config.BaseURL, config.APIKey, model, endpoint), nil
default:
// openai, chatfire 等其他厂商都使用 OpenAI 格式
return ai.NewOpenAIClient(config.BaseURL, config.APIKey, model, endpoint), nil
}
}
// GetAIClientForModel 根据服务类型和模型名称获取对应的AI客户端
func (s *AIService) GetAIClientForModel(serviceType string, modelName string) (ai.AIClient, error) {
config, err := s.GetConfigForModel(serviceType, modelName)
if err != nil {
return nil, err
}
// 使用数据库配置中的 endpoint,如果为空则根据 provider 设置默认值
endpoint := config.Endpoint
if endpoint == "" {
switch config.Provider {
case "gemini", "google":
endpoint = "/v1beta/models/{model}:generateContent"
default:
endpoint = "/chat/completions"
}
}
// 根据 provider 创建对应的客户端
switch config.Provider {
case "gemini", "google":
return ai.NewGeminiClient(config.BaseURL, config.APIKey, modelName, endpoint), nil
default:
// openai, chatfire 等其他厂商都使用 OpenAI 格式
return ai.NewOpenAIClient(config.BaseURL, config.APIKey, modelName, endpoint), nil
}
}
func (s *AIService) GenerateText(prompt string, systemPrompt string, options ...func(*ai.ChatCompletionRequest)) (string, error) {
client, err := s.GetAIClient("text")
if err != nil {
return "", fmt.Errorf("failed to get AI client: %w", err)
}
return client.GenerateText(prompt, systemPrompt, options...)
}
func (s *AIService) GenerateImage(prompt string, size string, n int) ([]string, error) {
client, err := s.GetAIClient("image")
if err != nil {
return nil, fmt.Errorf("failed to get AI client for image: %w", err)
}
return client.GenerateImage(prompt, size, n)
}
================================================
FILE: application/services/asset_duration_update.go
================================================
package services
import (
"fmt"
models "github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/infrastructure/storage"
)
// UpdateAssetDurationFromFile 从本地文件探测并更新视频Asset的时长
func (s *AssetService) UpdateAssetDurationFromFile(assetID uint, localFilePath string) error {
var asset models.Asset
if err := s.db.Where("id = ?", assetID).First(&asset).Error; err != nil {
return fmt.Errorf("asset not found")
}
if asset.Type != models.AssetTypeVideo {
return fmt.Errorf("asset is not a video")
}
if s.ffmpeg == nil {
return fmt.Errorf("ffmpeg not available")
}
duration, err := s.ffmpeg.GetVideoDuration(localFilePath)
if err != nil {
return fmt.Errorf("failed to probe video duration: %w", err)
}
durationInt := int(duration + 0.5)
if err := s.db.Model(&asset).Update("duration", durationInt).Error; err != nil {
return fmt.Errorf("failed to update duration: %w", err)
}
s.log.Infow("Updated asset duration from file",
"asset_id", assetID,
"duration", durationInt,
"file", localFilePath)
return nil
}
// UpdateAssetDurationFromURL 下载视频并探测时长
func (s *AssetService) UpdateAssetDurationFromURL(assetID uint, localStorage *storage.LocalStorage) error {
var asset models.Asset
if err := s.db.Where("id = ?", assetID).First(&asset).Error; err != nil {
return fmt.Errorf("asset not found")
}
if asset.Type != models.AssetTypeVideo {
return fmt.Errorf("asset is not a video")
}
if localStorage == nil {
return fmt.Errorf("local storage not available")
}
// 下载视频到本地
localPath, err := localStorage.DownloadFromURL(asset.URL, "videos")
if err != nil {
return fmt.Errorf("failed to download video: %w", err)
}
// 探测时长
return s.UpdateAssetDurationFromFile(assetID, localPath)
}
================================================
FILE: application/services/asset_service.go
================================================
package services
import (
"fmt"
"strconv"
"strings"
models "github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/infrastructure/external/ffmpeg"
"github.com/drama-generator/backend/pkg/logger"
"gorm.io/gorm"
)
type AssetService struct {
db *gorm.DB
log *logger.Logger
ffmpeg *ffmpeg.FFmpeg
}
func NewAssetService(db *gorm.DB, log *logger.Logger) *AssetService {
return &AssetService{
db: db,
log: log,
ffmpeg: ffmpeg.NewFFmpeg(log),
}
}
type CreateAssetRequest struct {
DramaID *string `json:"drama_id"`
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
Type models.AssetType `json:"type" binding:"required"`
Category *string `json:"category"`
URL string `json:"url" binding:"required"`
ThumbnailURL *string `json:"thumbnail_url"`
LocalPath *string `json:"local_path"`
FileSize *int64 `json:"file_size"`
MimeType *string `json:"mime_type"`
Width *int `json:"width"`
Height *int `json:"height"`
Duration *int `json:"duration"`
Format *string `json:"format"`
ImageGenID *uint `json:"image_gen_id"`
VideoGenID *uint `json:"video_gen_id"`
TagIDs []uint `json:"tag_ids"`
}
type UpdateAssetRequest struct {
Name *string `json:"name"`
Description *string `json:"description"`
Category *string `json:"category"`
ThumbnailURL *string `json:"thumbnail_url"`
TagIDs []uint `json:"tag_ids"`
IsFavorite *bool `json:"is_favorite"`
}
type ListAssetsRequest struct {
DramaID *string `json:"drama_id"`
EpisodeID *uint `json:"episode_id"`
StoryboardID *uint `json:"storyboard_id"`
Type *models.AssetType `json:"type"`
Category string `json:"category"`
TagIDs []uint `json:"tag_ids"`
IsFavorite *bool `json:"is_favorite"`
Search string `json:"search"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (s *AssetService) CreateAsset(req *CreateAssetRequest) (*models.Asset, error) {
var dramaID *uint
if req.DramaID != nil && *req.DramaID != "" {
id, err := strconv.ParseUint(*req.DramaID, 10, 32)
if err == nil {
uid := uint(id)
dramaID = &uid
}
}
if dramaID != nil {
var drama models.Drama
if err := s.db.Where("id = ?", *dramaID).First(&drama).Error; err != nil {
return nil, fmt.Errorf("drama not found")
}
}
asset := &models.Asset{
DramaID: dramaID,
Name: req.Name,
Description: req.Description,
Type: req.Type,
Category: req.Category,
URL: req.URL,
ThumbnailURL: req.ThumbnailURL,
LocalPath: req.LocalPath,
FileSize: req.FileSize,
MimeType: req.MimeType,
Width: req.Width,
Height: req.Height,
Duration: req.Duration,
Format: req.Format,
ImageGenID: req.ImageGenID,
VideoGenID: req.VideoGenID,
}
if err := s.db.Create(asset).Error; err != nil {
return nil, fmt.Errorf("failed to create asset: %w", err)
}
return asset, nil
}
func (s *AssetService) UpdateAsset(assetID uint, req *UpdateAssetRequest) (*models.Asset, error) {
var asset models.Asset
if err := s.db.Where("id = ?", assetID).First(&asset).Error; err != nil {
return nil, fmt.Errorf("asset not found")
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.ThumbnailURL != nil {
updates["thumbnail_url"] = *req.ThumbnailURL
}
if req.IsFavorite != nil {
updates["is_favorite"] = *req.IsFavorite
}
if len(updates) > 0 {
if err := s.db.Model(&asset).Updates(updates).Error; err != nil {
return nil, fmt.Errorf("failed to update asset: %w", err)
}
}
if err := s.db.First(&asset, assetID).Error; err != nil {
return nil, err
}
return &asset, nil
}
func (s *AssetService) GetAsset(assetID uint) (*models.Asset, error) {
var asset models.Asset
if err := s.db.Where("id = ? ", assetID).First(&asset).Error; err != nil {
return nil, err
}
s.db.Model(&asset).UpdateColumn("view_count", gorm.Expr("view_count + ?", 1))
return &asset, nil
}
func (s *AssetService) ListAssets(req *ListAssetsRequest) ([]models.Asset, int64, error) {
query := s.db.Model(&models.Asset{})
if req.DramaID != nil {
var dramaID uint64
dramaID, _ = strconv.ParseUint(*req.DramaID, 10, 32)
query = query.Where("drama_id = ?", uint(dramaID))
}
if req.EpisodeID != nil {
query = query.Where("episode_id = ?", *req.EpisodeID)
}
if req.StoryboardID != nil {
query = query.Where("storyboard_id = ?", *req.StoryboardID)
}
if req.Type != nil {
query = query.Where("type = ?", *req.Type)
}
if req.Category != "" {
query = query.Where("category = ?", req.Category)
}
if req.IsFavorite != nil {
query = query.Where("is_favorite = ?", *req.IsFavorite)
}
if req.Search != "" {
searchTerm := "%" + strings.ToLower(req.Search) + "%"
query = query.Where("LOWER(name) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var assets []models.Asset
offset := (req.Page - 1) * req.PageSize
if err := query.Order("created_at DESC").
Offset(offset).Limit(req.PageSize).Find(&assets).Error; err != nil {
return nil, 0, err
}
return assets, total, nil
}
func (s *AssetService) DeleteAsset(assetID uint) error {
result := s.db.Where("id = ?", assetID).Delete(&models.Asset{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("asset not found")
}
return nil
}
func (s *AssetService) ImportFromImageGen(imageGenID uint) (*models.Asset, error) {
var imageGen models.ImageGeneration
if err := s.db.Where("id = ? ", imageGenID).First(&imageGen).Error; err != nil {
return nil, fmt.Errorf("image generation not found")
}
if imageGen.Status != models.ImageStatusCompleted || imageGen.ImageURL == nil {
return nil, fmt.Errorf("image is not ready")
}
dramaID := imageGen.DramaID
asset := &models.Asset{
Name: fmt.Sprintf("Image_%d", imageGen.ID),
Type: models.AssetTypeImage,
URL: *imageGen.ImageURL,
DramaID: &dramaID,
ImageGenID: &imageGenID,
Width: imageGen.Width,
Height: imageGen.Height,
}
if err := s.db.Create(asset).Error; err != nil {
return nil, fmt.Errorf("failed to create asset: %w", err)
}
return asset, nil
}
func (s *AssetService) ImportFromVideoGen(videoGenID uint) (*models.Asset, error) {
var videoGen models.VideoGeneration
if err := s.db.Preload("Storyboard.Episode").Where("id = ? ", videoGenID).First(&videoGen).Error; err != nil {
return nil, fmt.Errorf("video generation not found")
}
if videoGen.Status != models.VideoStatusCompleted || videoGen.VideoURL == nil {
return nil, fmt.Errorf("video is not ready")
}
dramaID := videoGen.DramaID
var episodeID *uint
var storyboardNum *int
if videoGen.Storyboard != nil {
episodeID = &videoGen.Storyboard.Episode.ID
storyboardNum = &videoGen.Storyboard.StoryboardNumber
}
asset := &models.Asset{
Name: fmt.Sprintf("Video_%d", videoGen.ID),
Type: models.AssetTypeVideo,
URL: *videoGen.VideoURL,
LocalPath: videoGen.LocalPath, // 同步 local_path 到 assets 表
DramaID: &dramaID,
EpisodeID: episodeID,
StoryboardID: videoGen.StoryboardID,
StoryboardNum: storyboardNum,
VideoGenID: &videoGenID,
Duration: videoGen.Duration,
Width: videoGen.Width,
Height: videoGen.Height,
}
if videoGen.FirstFrameURL != nil {
asset.ThumbnailURL = videoGen.FirstFrameURL
}
if err := s.db.Create(asset).Error; err != nil {
return nil, fmt.Errorf("failed to create asset: %w", err)
}
return asset, nil
}
================================================
FILE: application/services/audio_extraction_service.go
================================================
package services
import (
"fmt"
"path/filepath"
"time"
"github.com/drama-generator/backend/infrastructure/external/ffmpeg"
"github.com/drama-generator/backend/pkg/logger"
)
type AudioExtractionService struct {
ffmpeg *ffmpeg.FFmpeg
log *logger.Logger
}
func NewAudioExtractionService(log *logger.Logger) *AudioExtractionService {
return &AudioExtractionService{
ffmpeg: ffmpeg.NewFFmpeg(log),
log: log,
}
}
type ExtractAudioRequest struct {
VideoURL string `json:"video_url" binding:"required"`
}
type ExtractAudioResponse struct {
AudioURL string `json:"audio_url"`
Duration float64 `json:"duration"`
}
// ExtractAudio 从视频URL提取音频并返回音频文件URL
func (s *AudioExtractionService) ExtractAudio(videoURL string, dataDir string) (*ExtractAudioResponse, error) {
s.log.Infow("Starting audio extraction", "video_url", videoURL)
// 生成输出文件名
timestamp := time.Now().Unix()
audioFileName := fmt.Sprintf("audio_%d.aac", timestamp)
audioOutputPath := filepath.Join(dataDir, "audios", audioFileName)
// 提取音频
extractedPath, err := s.ffmpeg.ExtractAudio(videoURL, audioOutputPath)
if err != nil {
s.log.Errorw("Failed to extract audio", "error", err, "video_url", videoURL)
return nil, fmt.Errorf("failed to extract audio: %w", err)
}
// 获取音频时长(使用提取后的本地文件路径)
duration, err := s.ffmpeg.GetVideoDuration(extractedPath)
if err != nil {
s.log.Errorw("Failed to get audio duration", "error", err, "path", extractedPath)
return nil, fmt.Errorf("failed to get audio duration: %w", err)
}
if duration <= 0 {
s.log.Errorw("Invalid audio duration", "duration", duration, "path", extractedPath)
return nil, fmt.Errorf("invalid audio duration: %.2f", duration)
}
// 构建音频URL(相对于data目录)
audioURL := fmt.Sprintf("/data/audios/%s", audioFileName)
s.log.Infow("Audio extraction completed",
"video_url", videoURL,
"audio_url", audioURL,
"duration", duration,
"local_path", extractedPath)
return &ExtractAudioResponse{
AudioURL: audioURL,
Duration: duration,
}, nil
}
// BatchExtractAudio 批量提取音频
func (s *AudioExtractionService) BatchExtractAudio(videoURLs []string, dataDir string) ([]*ExtractAudioResponse, error) {
s.log.Infow("Starting batch audio extraction", "count", len(videoURLs))
results := make([]*ExtractAudioResponse, 0, len(videoURLs))
for i, videoURL := range videoURLs {
s.log.Infow("Extracting audio", "index", i+1, "total", len(videoURLs), "video_url", videoURL)
result, err := s.ExtractAudio(videoURL, dataDir)
if err != nil {
s.log.Errorw("Failed to extract audio in batch", "index", i, "video_url", videoURL, "error", err)
// 继续处理其他视频,但记录错误
return nil, fmt.Errorf("failed to extract audio at index %d: %w", i, err)
}
results = append(results, result)
}
s.log.Infow("Batch audio extraction completed", "successful_count", len(results))
return results, nil
}
================================================
FILE: application/services/character_library_service.go
================================================
package services
import (
"errors"
"fmt"
"time"
models "github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/ai"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"github.com/drama-generator/backend/pkg/utils"
"gorm.io/gorm"
)
type CharacterLibraryService struct {
db *gorm.DB
log *logger.Logger
config *config.Config
aiService *AIService
taskService *TaskService
promptI18n *PromptI18n
}
func NewCharacterLibraryService(db *gorm.DB, log *logger.Logger, cfg *config.Config) *CharacterLibraryService {
return &CharacterLibraryService{
db: db,
log: log,
config: cfg,
aiService: NewAIService(db, log),
taskService: NewTaskService(db, log),
promptI18n: NewPromptI18n(cfg),
}
}
type CreateLibraryItemRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
Category *string `json:"category"`
ImageURL string `json:"image_url" binding:"required"`
LocalPath *string `json:"local_path"`
Description *string `json:"description"`
Tags *string `json:"tags"`
SourceType string `json:"source_type"`
}
type CharacterLibraryQuery struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
Category string `form:"category"`
SourceType string `form:"source_type"`
Keyword string `form:"keyword"`
}
// ListLibraryItems 获取用户角色库列表
func (s *CharacterLibraryService) ListLibraryItems(query *CharacterLibraryQuery) ([]models.CharacterLibrary, int64, error) {
var items []models.CharacterLibrary
var total int64
db := s.db.Model(&models.CharacterLibrary{})
// 筛选条件
if query.Category != "" {
db = db.Where("category = ?", query.Category)
}
if query.SourceType != "" {
db = db.Where("source_type = ?", query.SourceType)
}
if query.Keyword != "" {
db = db.Where("name LIKE ? OR description LIKE ?", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
// 获取总数
if err := db.Count(&total).Error; err != nil {
s.log.Errorw("Failed to count character library", "error", err)
return nil, 0, err
}
// 分页查询
offset := (query.Page - 1) * query.PageSize
err := db.Order("created_at DESC").
Offset(offset).
Limit(query.PageSize).
Find(&items).Error
if err != nil {
s.log.Errorw("Failed to list character library", "error", err)
return nil, 0, err
}
return items, total, nil
}
// CreateLibraryItem 添加到角色库
func (s *CharacterLibraryService) CreateLibraryItem(req *CreateLibraryItemRequest) (*models.CharacterLibrary, error) {
sourceType := req.SourceType
if sourceType == "" {
sourceType = "generated"
}
item := &models.CharacterLibrary{
Name: req.Name,
Category: req.Category,
ImageURL: req.ImageURL,
LocalPath: req.LocalPath,
Description: req.Description,
Tags: req.Tags,
SourceType: sourceType,
}
if err := s.db.Create(item).Error; err != nil {
s.log.Errorw("Failed to create library item", "error", err)
return nil, err
}
s.log.Infow("Library item created", "item_id", item.ID)
return item, nil
}
// GetLibraryItem 获取角色库项
func (s *CharacterLibraryService) GetLibraryItem(itemID string) (*models.CharacterLibrary, error) {
var item models.CharacterLibrary
err := s.db.Where("id = ? ", itemID).First(&item).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("library item not found")
}
s.log.Errorw("Failed to get library item", "error", err)
return nil, err
}
return &item, nil
}
// DeleteLibraryItem 删除角色库项
func (s *CharacterLibraryService) DeleteLibraryItem(itemID string) error {
result := s.db.Where("id = ? ", itemID).Delete(&models.CharacterLibrary{})
if result.Error != nil {
s.log.Errorw("Failed to delete library item", "error", result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("library item not found")
}
s.log.Infow("Library item deleted", "item_id", itemID)
return nil
}
// ApplyLibraryItemToCharacter 将角色库形象应用到角色
func (s *CharacterLibraryService) ApplyLibraryItemToCharacter(characterID string, libraryItemID string) error {
// 验证角色库项存在且属于该用户
var libraryItem models.CharacterLibrary
if err := s.db.Where("id = ? ", libraryItemID).First(&libraryItem).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("library item not found")
}
return err
}
// 查找角色
var character models.Character
if err := s.db.Where("id = ?", characterID).First(&character).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("character not found")
}
return err
}
// 查询Drama验证权限
var drama models.Drama
if err := s.db.Where("id = ? ", character.DramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("unauthorized")
}
return err
}
// 更新角色的 local_path 和 image_url
updates := map[string]interface{}{}
if libraryItem.LocalPath != nil && *libraryItem.LocalPath != "" {
updates["local_path"] = libraryItem.LocalPath
}
if libraryItem.ImageURL != "" {
updates["image_url"] = libraryItem.ImageURL
}
if len(updates) > 0 {
if err := s.db.Model(&character).Updates(updates).Error; err != nil {
s.log.Errorw("Failed to update character image", "error", err)
return err
}
}
s.log.Infow("Library item applied to character", "character_id", characterID, "library_item_id", libraryItemID)
return nil
}
// UploadCharacterImage 上传角色图片
func (s *CharacterLibraryService) UploadCharacterImage(characterID string, imageURL string) error {
// 查找角色
var character models.Character
if err := s.db.Where("id = ?", characterID).First(&character).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("character not found")
}
return err
}
// 查询Drama验证权限
var drama models.Drama
if err := s.db.Where("id = ? ", character.DramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("unauthorized")
}
return err
}
// 更新图片URL
if err := s.db.Model(&character).Update("image_url", imageURL).Error; err != nil {
s.log.Errorw("Failed to update character image", "error", err)
return err
}
s.log.Infow("Character image uploaded", "character_id", characterID)
return nil
}
// AddCharacterToLibrary 将角色添加到角色库
func (s *CharacterLibraryService) AddCharacterToLibrary(characterID string, category *string) (*models.CharacterLibrary, error) {
// 查找角色
var character models.Character
if err := s.db.Where("id = ?", characterID).First(&character).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("character not found")
}
return nil, err
}
// 查询Drama验证权限
var drama models.Drama
if err := s.db.Where("id = ? ", character.DramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("unauthorized")
}
return nil, err
}
// 检查是否有图片
if character.ImageURL == nil || *character.ImageURL == "" {
return nil, fmt.Errorf("角色还没有形象图片")
}
// 创建角色库项
charLibrary := &models.CharacterLibrary{
Name: character.Name,
ImageURL: *character.ImageURL,
LocalPath: character.LocalPath,
Description: character.Description,
SourceType: "character",
}
if err := s.db.Create(charLibrary).Error; err != nil {
s.log.Errorw("Failed to add character to library", "error", err)
return nil, err
}
s.log.Infow("Character added to library", "character_id", characterID, "library_item_id", charLibrary.ID)
return charLibrary, nil
}
// DeleteCharacter 删除单个角色
func (s *CharacterLibraryService) DeleteCharacter(characterID uint) error {
// 查找角色
var character models.Character
if err := s.db.Where("id = ?", characterID).First(&character).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("character not found")
}
return err
}
// 验证权限:检查角色所属的drama是否属于当前用户
var drama models.Drama
if err := s.db.Where("id = ? ", character.DramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("unauthorized")
}
return err
}
// 删除角色
if err := s.db.Delete(&character).Error; err != nil {
s.log.Errorw("Failed to delete character", "error", err, "id", characterID)
return err
}
s.log.Infow("Character deleted", "id", characterID)
return nil
}
// GenerateCharacterImage AI生成角色形象
func (s *CharacterLibraryService) GenerateCharacterImage(characterID string, imageService *ImageGenerationService, modelName string, style string) (*models.ImageGeneration, error) {
// 查找角色
var character models.Character
if err := s.db.Where("id = ?", characterID).First(&character).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("character not found")
}
return nil, err
}
// 查询Drama验证权限
var drama models.Drama
if err := s.db.Where("id = ? ", character.DramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("unauthorized")
}
return nil, err
}
// 构建生成提示词 - 使用详细的外貌描述,添加干净背景要求
prompt := ""
// 优先使用appearance字段,它包含了最详细的外貌描述
if character.Appearance != nil && *character.Appearance != "" {
prompt = *character.Appearance
} else if character.Description != nil && *character.Description != "" {
prompt = *character.Description
} else {
prompt = character.Name
}
// 使用已经加载的 drama 的 style 信息
if drama.Style != "" && drama.Style != "realistic" {
prompt += ", " + drama.Style
}
// 调用图片生成服务
dramaIDStr := fmt.Sprintf("%d", character.DramaID)
imageType := "character"
req := &GenerateImageRequest{
DramaID: dramaIDStr,
CharacterID: &character.ID,
ImageType: imageType,
Prompt: prompt,
Provider: "openai", // 或从配置读取
Model: modelName, // 使用用户指定的模型
Size: "2560x1440", // 3,686,400像素,满足API最低要求(16:9比例)
Quality: "standard",
}
imageGen, err := imageService.GenerateImage(req)
if err != nil {
s.log.Errorw("Failed to generate character image", "error", err)
return nil, fmt.Errorf("图片生成失败: %w", err)
}
// 异步处理:在后台监听图片生成完成,然后更新角色image_url
go s.waitAndUpdateCharacterImage(character.ID, imageGen.ID)
// 立即返回ImageGeneration对象,让前端可以轮询状态
s.log.Infow("Character image generation started", "character_id", characterID, "image_gen_id", imageGen.ID)
return imageGen, nil
}
// waitAndUpdateCharacterImage 后台异步等待图片生成完成并更新角色image_url
func (s *CharacterLibraryService) waitAndUpdateCharacterImage(characterID uint, imageGenID uint) {
maxAttempts := 60
pollInterval := 5 * time.Second
for i := 0; i < maxAttempts; i++ {
time.Sleep(pollInterval)
// 查询图片生成状态
var imageGen models.ImageGeneration
if err := s.db.First(&imageGen, imageGenID).Error; err != nil {
s.log.Errorw("Failed to query image generation status", "error", err, "image_gen_id", imageGenID)
continue
}
// 检查是否完成
if imageGen.Status == models.ImageStatusCompleted && imageGen.ImageURL != nil && *imageGen.ImageURL != "" {
// 更新角色的image_url
if err := s.db.Model(&models.Character{}).Where("id = ?", characterID).Update("image_url", *imageGen.ImageURL).Error; err != nil {
s.log.Errorw("Failed to update character image_url", "error", err, "character_id", characterID)
return
}
s.log.Infow("Character image updated successfully", "character_id", characterID, "image_url", *imageGen.ImageURL)
return
}
// 检查是否失败
if imageGen.Status == models.ImageStatusFailed {
s.log.Errorw("Character image generation failed", "character_id", characterID, "image_gen_id", imageGenID, "error", imageGen.ErrorMsg)
return
}
}
s.log.Warnw("Character image generation timeout", "character_id", characterID, "image_gen_id", imageGenID)
}
type UpdateCharacterRequest struct {
Name *string `json:"name"`
Role *string `json:"role"`
Appearance *string `json:"appearance"`
Personality *string `json:"personality"`
Description *string `json:"description"`
ImageURL *string `json:"image_url"`
LocalPath *string `json:"local_path"`
}
// UpdateCharacter 更新角色信息
func (s *CharacterLibraryService) UpdateCharacter(characterID string, req *UpdateCharacterRequest) error {
// 查找角色
var character models.Character
if err := s.db.Where("id = ?", characterID).First(&character).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("character not found")
}
return err
}
// 验证权限:查询角色所属的drama是否属于该用户
var drama models.Drama
if err := s.db.Where("id = ? ", character.DramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("unauthorized")
}
return err
}
// 构建更新数据
updates := make(map[string]interface{})
if req.Name != nil && *req.Name != "" {
updates["name"] = *req.Name
}
if req.Role != nil {
updates["role"] = *req.Role
}
if req.Appearance != nil {
updates["appearance"] = *req.Appearance
}
if req.Personality != nil {
updates["personality"] = *req.Personality
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.ImageURL != nil {
updates["image_url"] = *req.ImageURL
}
if req.LocalPath != nil {
updates["local_path"] = *req.LocalPath
}
if len(updates) == 0 {
return errors.New("no fields to update")
}
// 更新角色信息
if err := s.db.Model(&character).Updates(updates).Error; err != nil {
s.log.Errorw("Failed to update character", "error", err, "character_id", characterID)
return err
}
s.log.Infow("Character updated", "character_id", characterID, "updates", updates)
return nil
}
// BatchGenerateCharacterImages 批量生成角色图片(并发执行)
func (s *CharacterLibraryService) BatchGenerateCharacterImages(characterIDs []string, imageService *ImageGenerationService, modelName string) {
s.log.Infow("Starting batch character image generation",
"count", len(characterIDs),
"model", modelName)
// 使用 goroutine 并发生成所有角色图片
for _, characterID := range characterIDs {
// 为每个角色启动单独的 goroutine
go func(charID string) {
imageGen, err := s.GenerateCharacterImage(charID, imageService, modelName, "") // 批量生成暂不支持自定义风格,使用默认值
if err != nil {
s.log.Errorw("Failed to generate character image in batch",
"character_id", charID,
"error", err)
return
}
s.log.Infow("Character image generated in batch",
"character_id", charID,
"image_gen_id", imageGen.ID)
}(characterID)
}
s.log.Infow("Batch character image generation tasks submitted",
"total", len(characterIDs))
}
// ExtractCharactersFromScript 从分集剧本中提取角色
func (s *CharacterLibraryService) ExtractCharactersFromScript(episodeID uint) (string, error) {
var episode models.Episode
if err := s.db.First(&episode, episodeID).Error; err != nil {
return "", fmt.Errorf("episode not found")
}
if episode.ScriptContent == nil || *episode.ScriptContent == "" {
return "", fmt.Errorf("剧本内容为空")
}
task, err := s.taskService.CreateTask("character_extraction", fmt.Sprintf("%d", episode.DramaID))
if err != nil {
return "", fmt.Errorf("创建任务失败: %w", err)
}
go s.processCharacterExtraction(task.ID, episode)
return task.ID, nil
}
func (s *CharacterLibraryService) processCharacterExtraction(taskID string, episode models.Episode) {
s.taskService.UpdateTaskStatus(taskID, "processing", 0, "正在分析剧本...")
script := ""
if episode.ScriptContent != nil {
script = *episode.ScriptContent
}
// 获取 drama 的 style 信息
var drama models.Drama
if err := s.db.First(&drama, episode.DramaID).Error; err != nil {
s.log.Warnw("Failed to load drama", "error", err, "drama_id", episode.DramaID)
}
prompt := s.promptI18n.GetCharacterExtractionPrompt(drama.Style)
userPrompt := fmt.Sprintf("【剧本内容】\n%s", script)
response, err := s.aiService.GenerateText(userPrompt, prompt, ai.WithMaxTokens(3000))
if err != nil {
s.taskService.UpdateTaskError(taskID, err)
return
}
s.taskService.UpdateTaskStatus(taskID, "processing", 50, "正在整理角色数据...")
var extractedCharacters []struct {
Name string `json:"name"`
Role string `json:"role"`
Appearance string `json:"appearance"`
Personality string `json:"personality"`
Description string `json:"description"`
}
if err := utils.SafeParseAIJSON(response, &extractedCharacters); err != nil {
s.log.Errorw("Failed to parse AI response for characters", "error", err, "response", response)
s.taskService.UpdateTaskError(taskID, fmt.Errorf("解析AI响应失败"))
return
}
var savedCharacters []models.Character
for _, charData := range extractedCharacters {
// 检查是否已存在同名角色
var existingCharacter models.Character
err := s.db.Where("drama_id = ? AND name = ?", episode.DramaID, charData.Name).First(&existingCharacter).Error
if err == nil {
// 如果存在,只关联,不更新(或者可以选更新,这里暂不更新)
if err := s.db.Model(&episode).Association("Characters").Append(&existingCharacter); err != nil {
s.log.Warnw("Failed to associate existing character", "error", err)
}
savedCharacters = append(savedCharacters, existingCharacter)
} else {
// 创建新角色
newCharacter := models.Character{
DramaID: episode.DramaID,
Name: charData.Name,
Role: &charData.Role,
Appearance: &charData.Appearance,
Personality: &charData.Personality,
Description: &charData.Description,
}
if err := s.db.Create(&newCharacter).Error; err != nil {
s.log.Errorw("Failed to create extracted character", "error", err)
continue
}
// 关联到分集
if err := s.db.Model(&episode).Association("Characters").Append(&newCharacter); err != nil {
s.log.Warnw("Failed to associate new character", "error", err)
}
savedCharacters = append(savedCharacters, newCharacter)
}
}
s.taskService.UpdateTaskResult(taskID, map[string]interface{}{
"characters": savedCharacters,
"count": len(savedCharacters),
})
}
================================================
FILE: application/services/data_migration_service.go
================================================
package services
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/logger"
"gorm.io/gorm"
)
type DataMigrationService struct {
db *gorm.DB
log *logger.Logger
storageRoot string
urlMapping map[string]string // 原始URL -> 本地路径的映射
}
func NewDataMigrationService(db *gorm.DB, log *logger.Logger) *DataMigrationService {
return &DataMigrationService{
db: db,
log: log,
storageRoot: "data/storage",
urlMapping: make(map[string]string),
}
}
// MigrateLocalPaths 迁移所有表中 local_path 为空的数据
func (s *DataMigrationService) MigrateLocalPaths() error {
s.log.Info("开始数据清洗:迁移 local_path 为空的数据")
startTime := time.Now()
// 确保存储目录存在
if err := s.ensureStorageDirectories(); err != nil {
return fmt.Errorf("创建存储目录失败: %w", err)
}
// 迁移各个表的数据(按指定顺序)
stats := &MigrationStats{}
// 1. 迁移 assets 表
if err := s.migrateAssets(stats); err != nil {
s.log.Errorw("迁移 assets 数据失败", "error", err)
}
// 2. 迁移 character_libraries 表
if err := s.migrateCharacterLibraries(stats); err != nil {
s.log.Errorw("迁移 character_libraries 数据失败", "error", err)
}
// 3. 迁移 characters 表
if err := s.migrateCharacters(stats); err != nil {
s.log.Errorw("迁移 characters 数据失败", "error", err)
}
// 4. 迁移 image_generations 表
if err := s.migrateImageGenerations(stats); err != nil {
s.log.Errorw("迁移 image_generations 数据失败", "error", err)
}
// 5. 迁移 scenes 表
if err := s.migrateScenes(stats); err != nil {
s.log.Errorw("迁移 scenes 数据失败", "error", err)
}
// 6. 迁移 video_generations 表
if err := s.migrateVideoGenerations(stats); err != nil {
s.log.Errorw("迁移 video_generations 数据失败", "error", err)
}
duration := time.Since(startTime)
s.log.Infow("数据清洗完成",
"总耗时", duration.String(),
"URL映射缓存数", len(s.urlMapping),
"Assets成功", stats.AssetsSuccess,
"Assets失败", stats.AssetsFailed,
"角色库成功", stats.CharacterLibrariesSuccess,
"角色库失败", stats.CharacterLibrariesFailed,
"角色成功", stats.CharactersSuccess,
"角色失败", stats.CharactersFailed,
"图片生成成功", stats.ImageGenerationsSuccess,
"图片生成失败", stats.ImageGenerationsFailed,
"场景成功", stats.ScenesSuccess,
"场景失败", stats.ScenesFailed,
"视频成功", stats.VideosSuccess,
"视频失败", stats.VideosFailed,
)
return nil
}
// MigrationStats 迁移统计信息
type MigrationStats struct {
AssetsSuccess int
AssetsFailed int
CharacterLibrariesSuccess int
CharacterLibrariesFailed int
CharactersSuccess int
CharactersFailed int
ImageGenerationsSuccess int
ImageGenerationsFailed int
ScenesSuccess int
ScenesFailed int
VideosSuccess int
VideosFailed int
}
// ensureStorageDirectories 确保存储目录存在
func (s *DataMigrationService) ensureStorageDirectories() error {
dirs := []string{
filepath.Join(s.storageRoot, "images"),
filepath.Join(s.storageRoot, "characters"),
filepath.Join(s.storageRoot, "videos"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建目录 %s 失败: %w", dir, err)
}
}
s.log.Infow("存储目录创建成功", "root", s.storageRoot)
return nil
}
// migrateAssets 迁移 assets 表数据
func (s *DataMigrationService) migrateAssets(stats *MigrationStats) error {
s.log.Info("开始迁移 assets 数据...")
var assets []models.Asset
// 查询 local_path 为空但 url 不为空的资源
if err := s.db.Where("(local_path IS NULL OR local_path = '') AND url IS NOT NULL AND url != ''").Find(&assets).Error; err != nil {
return fmt.Errorf("查询 assets 数据失败: %w", err)
}
s.log.Infow("找到需要迁移的 assets", "数量", len(assets))
for _, asset := range assets {
s.log.Infow("处理 asset", "id", asset.ID, "name", asset.Name, "type", asset.Type, "url", asset.URL)
// 根据类型选择存储目录
subDir := "images"
if asset.Type == models.AssetTypeVideo {
subDir = "videos"
}
localPath, err := s.downloadOrGetCached(asset.URL, subDir, fmt.Sprintf("asset_%d", asset.ID))
if err != nil {
s.log.Errorw("下载 asset 失败", "asset_id", asset.ID, "error", err)
stats.AssetsFailed++
continue
}
// 更新 local_path
if err := s.db.Model(&asset).Update("local_path", localPath).Error; err != nil {
s.log.Errorw("更新 asset local_path 失败", "asset_id", asset.ID, "error", err)
stats.AssetsFailed++
continue
}
s.log.Infow("asset 迁移成功", "asset_id", asset.ID, "local_path", localPath)
stats.AssetsSuccess++
}
return nil
}
// migrateCharacterLibraries 迁移 character_libraries 表数据
func (s *DataMigrationService) migrateCharacterLibraries(stats *MigrationStats) error {
s.log.Info("开始迁移 character_libraries 数据...")
var charLibs []models.CharacterLibrary
// 查询 local_path 为空但 image_url 不为空的角色库
if err := s.db.Where("(local_path IS NULL OR local_path = '') AND image_url IS NOT NULL AND image_url != ''").Find(&charLibs).Error; err != nil {
return fmt.Errorf("查询 character_libraries 数据失败: %w", err)
}
s.log.Infow("找到需要迁移的 character_libraries", "数量", len(charLibs))
for _, charLib := range charLibs {
s.log.Infow("处理 character_library", "id", charLib.ID, "name", charLib.Name, "image_url", charLib.ImageURL)
localPath, err := s.downloadOrGetCached(charLib.ImageURL, "characters", fmt.Sprintf("charlib_%d", charLib.ID))
if err != nil {
s.log.Errorw("下载 character_library 图片失败", "charlib_id", charLib.ID, "error", err)
stats.CharacterLibrariesFailed++
continue
}
// 更新 local_path
if err := s.db.Model(&charLib).Update("local_path", localPath).Error; err != nil {
s.log.Errorw("更新 character_library local_path 失败", "charlib_id", charLib.ID, "error", err)
stats.CharacterLibrariesFailed++
continue
}
s.log.Infow("character_library 迁移成功", "charlib_id", charLib.ID, "local_path", localPath)
stats.CharacterLibrariesSuccess++
}
return nil
}
// migrateImageGenerations 迁移 image_generations 表数据
func (s *DataMigrationService) migrateImageGenerations(stats *MigrationStats) error {
s.log.Info("开始迁移 image_generations 数据...")
var imageGens []models.ImageGeneration
// 查询 local_path 为空但 image_url 不为空的图片生成记录
if err := s.db.Where("(local_path IS NULL OR local_path = '') AND image_url IS NOT NULL AND image_url != ''").Find(&imageGens).Error; err != nil {
return fmt.Errorf("查询 image_generations 数据失败: %w", err)
}
s.log.Infow("找到需要迁移的 image_generations", "数量", len(imageGens))
for _, imageGen := range imageGens {
if imageGen.ImageURL == nil {
continue
}
imageTypeStr := string(imageGen.ImageType)
s.log.Infow("处理 image_generation", "id", imageGen.ID, "image_type", imageTypeStr, "image_url", *imageGen.ImageURL)
// 根据图片类型选择存储目录
subDir := "images"
if imageGen.ImageType == "character" {
subDir = "characters"
}
localPath, err := s.downloadOrGetCached(*imageGen.ImageURL, subDir, fmt.Sprintf("imggen_%d", imageGen.ID))
if err != nil {
s.log.Errorw("下载 image_generation 图片失败", "imggen_id", imageGen.ID, "error", err)
stats.ImageGenerationsFailed++
continue
}
// 更新 local_path
if err := s.db.Model(&imageGen).Update("local_path", localPath).Error; err != nil {
s.log.Errorw("更新 image_generation local_path 失败", "imggen_id", imageGen.ID, "error", err)
stats.ImageGenerationsFailed++
continue
}
s.log.Infow("image_generation 迁移成功", "imggen_id", imageGen.ID, "local_path", localPath)
stats.ImageGenerationsSuccess++
}
return nil
}
// migrateScenes 迁移场景数据
func (s *DataMigrationService) migrateScenes(stats *MigrationStats) error {
s.log.Info("开始迁移场景数据...")
var scenes []models.Scene
// 查询 local_path 为空但 image_url 不为空的场景
if err := s.db.Where("(local_path IS NULL OR local_path = '') AND image_url IS NOT NULL AND image_url != ''").Find(&scenes).Error; err != nil {
return fmt.Errorf("查询场景数据失败: %w", err)
}
s.log.Infow("找到需要迁移的场景", "数量", len(scenes))
for _, scene := range scenes {
if scene.ImageURL == nil {
continue
}
s.log.Infow("处理场景", "id", scene.ID, "location", scene.Location, "image_url", *scene.ImageURL)
localPath, err := s.downloadOrGetCached(*scene.ImageURL, "images", fmt.Sprintf("scene_%d", scene.ID))
if err != nil {
s.log.Errorw("下载场景图片失败", "scene_id", scene.ID, "error", err)
stats.ScenesFailed++
continue
}
// 更新 local_path
if err := s.db.Model(&scene).Update("local_path", localPath).Error; err != nil {
s.log.Errorw("更新场景 local_path 失败", "scene_id", scene.ID, "error", err)
stats.ScenesFailed++
continue
}
s.log.Infow("场景迁移成功", "scene_id", scene.ID, "local_path", localPath)
stats.ScenesSuccess++
}
return nil
}
// migrateCharacters 迁移角色数据
func (s *DataMigrationService) migrateCharacters(stats *MigrationStats) error {
s.log.Info("开始迁移角色数据...")
var characters []models.Character
// 查询 local_path 为空但 image_url 不为空的角色
if err := s.db.Where("(local_path IS NULL OR local_path = '') AND image_url IS NOT NULL AND image_url != ''").Find(&characters).Error; err != nil {
return fmt.Errorf("查询角色数据失败: %w", err)
}
s.log.Infow("找到需要迁移的角色", "数量", len(characters))
for _, character := range characters {
if character.ImageURL == nil {
continue
}
s.log.Infow("处理角色", "id", character.ID, "name", character.Name, "image_url", *character.ImageURL)
localPath, err := s.downloadOrGetCached(*character.ImageURL, "characters", fmt.Sprintf("character_%d", character.ID))
if err != nil {
s.log.Errorw("下载角色图片失败", "character_id", character.ID, "error", err)
stats.CharactersFailed++
continue
}
// 更新 local_path
if err := s.db.Model(&character).Update("local_path", localPath).Error; err != nil {
s.log.Errorw("更新角色 local_path 失败", "character_id", character.ID, "error", err)
stats.CharactersFailed++
continue
}
s.log.Infow("角色迁移成功", "character_id", character.ID, "local_path", localPath)
stats.CharactersSuccess++
}
return nil
}
// migrateVideoGenerations 迁移视频生成数据
func (s *DataMigrationService) migrateVideoGenerations(stats *MigrationStats) error {
s.log.Info("开始迁移视频生成数据...")
var videoGens []models.VideoGeneration
// 查询 local_path 为空但 video_url 不为空的视频
if err := s.db.Where("(local_path IS NULL OR local_path = '') AND video_url IS NOT NULL AND video_url != ''").Find(&videoGens).Error; err != nil {
return fmt.Errorf("查询视频生成数据失败: %w", err)
}
s.log.Infow("找到需要迁移的视频", "数量", len(videoGens))
for _, videoGen := range videoGens {
if videoGen.VideoURL == nil {
continue
}
s.log.Infow("处理视频", "id", videoGen.ID, "video_url", *videoGen.VideoURL)
localPath, err := s.downloadOrGetCached(*videoGen.VideoURL, "videos", fmt.Sprintf("video_%d", videoGen.ID))
if err != nil {
s.log.Errorw("下载视频失败", "video_gen_id", videoGen.ID, "error", err)
stats.VideosFailed++
continue
}
// 更新 local_path
if err := s.db.Model(&videoGen).Update("local_path", localPath).Error; err != nil {
s.log.Errorw("更新视频 local_path 失败", "video_gen_id", videoGen.ID, "error", err)
stats.VideosFailed++
continue
}
s.log.Infow("视频迁移成功", "video_gen_id", videoGen.ID, "local_path", localPath)
stats.VideosSuccess++
}
return nil
}
// downloadOrGetCached 下载文件或从缓存获取本地路径
func (s *DataMigrationService) downloadOrGetCached(url, subDir, prefix string) (string, error) {
// 1. 检查 URL 映射缓存
if localPath, exists := s.urlMapping[url]; exists {
s.log.Infow("使用缓存的本地路径", "url", url, "local_path", localPath)
return localPath, nil
}
// 2. 如果缓存中没有,则下载文件
var localPath string
var err error
// 根据子目录判断是图片还是视频
if subDir == "videos" {
localPath, err = s.downloadAndSaveVideo(url, subDir, prefix)
} else {
localPath, err = s.downloadAndSaveImage(url, subDir, prefix)
}
if err != nil {
return "", err
}
// 3. 将 URL 和本地路径的映射关系存入缓存
s.urlMapping[url] = localPath
s.log.Infow("已缓存 URL 映射", "url", url, "local_path", localPath)
return localPath, nil
}
// downloadAndSaveImage 下载并保存图片
func (s *DataMigrationService) downloadAndSaveImage(imageURL, subDir, prefix string) (string, error) {
if imageURL == "" {
return "", fmt.Errorf("图片 URL 为空")
}
// 如果已经是本地路径,直接返回
if strings.HasPrefix(imageURL, "/static/") || strings.HasPrefix(imageURL, "data/") {
return imageURL, nil
}
// 从 URL 中提取文件扩展名(去掉查询参数)
ext := s.extractFileExtension(imageURL)
// 生成文件名
timestamp := time.Now().Unix()
filename := fmt.Sprintf("%s_%d%s", prefix, timestamp, ext)
relativePath := filepath.Join(subDir, filename)
fullPath := filepath.Join(s.storageRoot, relativePath)
// 下载文件
if err := s.downloadFile(imageURL, fullPath); err != nil {
return "", fmt.Errorf("下载文件失败: %w", err)
}
// 返回相对路径(用于存储到数据库)
return relativePath, nil
}
// downloadAndSaveVideo 下载并保存视频
func (s *DataMigrationService) downloadAndSaveVideo(videoURL, subDir, prefix string) (string, error) {
if videoURL == "" {
return "", fmt.Errorf("视频 URL 为空")
}
// 如果已经是本地路径,直接返回
if strings.HasPrefix(videoURL, "/static/") || strings.HasPrefix(videoURL, "data/") {
return videoURL, nil
}
// 从 URL 中提取文件扩展名(去掉查询参数)
ext := s.extractFileExtension(videoURL)
if ext == "" || ext == ".jpeg" || ext == ".jpg" || ext == ".png" {
ext = ".mp4" // 视频默认扩展名
}
// 生成文件名
timestamp := time.Now().Unix()
filename := fmt.Sprintf("%s_%d%s", prefix, timestamp, ext)
relativePath := filepath.Join(subDir, filename)
fullPath := filepath.Join(s.storageRoot, relativePath)
// 下载文件
if err := s.downloadFile(videoURL, fullPath); err != nil {
return "", fmt.Errorf("下载文件失败: %w", err)
}
// 返回相对路径(用于存储到数据库)
return relativePath, nil
}
// extractFileExtension 从 URL 中提取文件扩展名(去掉查询参数)
func (s *DataMigrationService) extractFileExtension(url string) string {
// 去掉查询参数
if idx := strings.Index(url, "?"); idx != -1 {
url = url[:idx]
}
// 去掉 fragment
if idx := strings.Index(url, "#"); idx != -1 {
url = url[:idx]
}
// 获取文件扩展名
ext := filepath.Ext(url)
if ext == "" {
// 如果没有扩展名,默认返回 .jpg
return ".jpg"
}
// 转换为小写
ext = strings.ToLower(ext)
// 验证扩展名是否合理(限制长度)
if len(ext) > 10 {
return ".jpg"
}
return ext
}
// downloadFile 下载文件到指定路径
func (s *DataMigrationService) downloadFile(url, filepath string) error {
s.log.Infow("开始下载文件", "url", url, "filepath", filepath)
// 创建 HTTP 请求
client := &http.Client{
Timeout: 60 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP 状态码错误: %d", resp.StatusCode)
}
// 创建文件
out, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
defer out.Close()
// 复制内容
written, err := io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("写入文件失败: %w", err)
}
s.log.Infow("文件下载成功", "filepath", filepath, "size", written)
return nil
}
================================================
FILE: application/services/drama_service.go
================================================
package services
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/drama-generator/backend/domain/models"
"github.com/drama-generator/backend/pkg/config"
"github.com/drama-generator/backend/pkg/logger"
"gorm.io/gorm"
)
type DramaService struct {
db *gorm.DB
log *logger.Logger
baseURL string
}
func NewDramaService(db *gorm.DB, cfg *config.Config, log *logger.Logger) *DramaService {
return &DramaService{
db: db,
log: log,
baseURL: cfg.Storage.BaseURL,
}
}
type CreateDramaRequest struct {
Title string `json:"title" binding:"required,min=1,max=100"`
Description string `json:"description"`
Genre string `json:"genre"`
Style string `json:"style"`
Tags string `json:"tags"`
}
type UpdateDramaRequest struct {
Title string `json:"title" binding:"omitempty,min=1,max=100"`
Description string `json:"description"`
Genre string `json:"genre"`
Style string `json:"style"`
Tags string `json:"tags"`
Status string `json:"status" binding:"omitempty,oneof=draft planning production completed archived"`
}
type DramaListQuery struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
Status string `form:"status"`
Genre string `form:"genre"`
Keyword string `form:"keyword"`
}
func (s *DramaService) CreateDrama(req *CreateDramaRequest) (*models.Drama, error) {
drama := &models.Drama{
Title: req.Title,
Status: "draft",
Style: "ghibli", // 默认风格
}
if req.Description != "" {
drama.Description = &req.Description
}
if req.Genre != "" {
drama.Genre = &req.Genre
}
if req.Style != "" {
drama.Style = req.Style
}
if err := s.db.Create(drama).Error; err != nil {
s.log.Errorw("Failed to create drama", "error", err)
return nil, err
}
s.log.Infow("Drama created", "drama_id", drama.ID)
return drama, nil
}
func (s *DramaService) GetDrama(dramaID string) (*models.Drama, error) {
var drama models.Drama
err := s.db.Where("id = ? ", dramaID).
Preload("Characters"). // 加载Drama级别的角色
Preload("Scenes"). // 加载Drama级别的场景
Preload("Props"). // 加载Drama级别的道具
Preload("Episodes.Characters"). // 加载每个章节关联的角色
Preload("Episodes.Scenes"). // 加载每个章节关联的场景
Preload("Episodes.Storyboards", func(db *gorm.DB) *gorm.DB {
return db.Order("storyboards.storyboard_number ASC")
}).
Preload("Episodes.Storyboards.Props"). // 加载分镜关联的道具
First(&drama).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("drama not found")
}
s.log.Errorw("Failed to get drama", "error", err)
return nil, err
}
// 统计每个剧集的时长(基于场景时长之和)
for i := range drama.Episodes {
totalDuration := 0
for _, scene := range drama.Episodes[i].Storyboards {
totalDuration += scene.Duration
}
// 更新剧集时长(秒转分钟,向上取整)
durationMinutes := (totalDuration + 59) / 60
drama.Episodes[i].Duration = durationMinutes
// 如果数据库中的时长与计算的不一致,更新数据库
if drama.Episodes[i].Duration != durationMinutes {
s.db.Model(&models.Episode{}).Where("id = ?", drama.Episodes[i].ID).Update("duration", durationMinutes)
}
// 查询角色的图片生成状态
for j := range drama.Episodes[i].Characters {
var imageGen models.ImageGeneration
// 查询进行中或失败的任务状态
err := s.db.Where("character_id = ? AND (status = ? OR status = ?)",
drama.Episodes[i].Characters[j].ID, "pending", "processing").
Order("created_at DESC").
First(&imageGen).Error
if err == nil {
// 找到生成中的记录,设置状态
statusStr := string(imageGen.Status)
drama.Episodes[i].Characters[j].ImageGenerationStatus = &statusStr
if imageGen.ErrorMsg != nil {
drama.Episodes[i].Characters[j].ImageGenerationError = imageGen.ErrorMsg
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
// 检查是否有失败的记录
err := s.db.Where("character_id = ? AND status = ?",
drama.Episodes[i].Characters[j].ID, "failed").
Order("created_at DESC").
First(&imageGen).Error
if err == nil {
statusStr := string(imageGen.Status)
drama.Episodes[i].Characters[j].ImageGenerationStatus = &statusStr
if imageGen.ErrorMsg != nil {
drama.Episodes[i].Characters[j].ImageGenerationError = imageGen.ErrorMsg
}
}
}
}
// 查询场景的图片生成状态
for j := range drama.Episodes[i].Scenes {
var imageGen models.ImageGeneration
// 查询进行中或失败的任务状态
err := s.db.Where("scene_id = ? AND (status = ? OR status = ?)",
drama.Episodes[i].Scenes[j].ID, "pending", "processing").
Order("created_at DESC").
First(&imageGen).Error
if err == nil {
// 找到生成中的记录,设置状态
statusStr := string(imageGen.Status)
drama.Episodes[i].Scenes[j].ImageGenerationStatus = &statusStr
if imageGen.ErrorMsg != nil {
drama.Episodes[i].Scenes[j].ImageGenerationError = imageGen.ErrorMsg
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
// 检查是否有失败的记录
err := s.db.Where("scene_id = ? AND status = ?",
drama.Episodes[i].Scenes[j].ID, "failed").
Order("created_at DESC").
First(&imageGen).Error
if err == nil {
statusStr := string(imageGen.Status)
drama.Episodes[i].Scenes[j].ImageGenerationStatus = &statusStr
if imageGen.ErrorMsg != nil {
drama.Episodes[i].Scenes[j].ImageGenerationError = imageGen.ErrorMsg
}
}
}
}
}
// 整合所有剧集的场景到Drama级别的Scenes字段
sceneMap := make(map[uint]*models.Scene) // 用于去重
for i := range drama.Episodes {
for j := range drama.Episodes[i].Scenes {
scene := &drama.Episodes[i].Scenes[j]
sceneMap[scene.ID] = scene
}
}
// 将整合的场景添加到drama.Scenes
drama.Scenes = make([]models.Scene, 0, len(sceneMap))
for _, scene := range sceneMap {
drama.Scenes = append(drama.Scenes, *scene)
}
// 为所有场景的 local_path 添加 base_url 前缀
// s.addBaseURLToScenes(&drama)
return &drama, nil
}
func (s *DramaService) ListDramas(query *DramaListQuery) ([]models.Drama, int64, error) {
var dramas []models.Drama
var total int64
db := s.db.Model(&models.Drama{})
if query.Status != "" {
db = db.Where("status = ?", query.Status)
}
if query.Genre != "" {
db = db.Where("genre = ?", query.Genre)
}
if query.Keyword != "" {
db = db.Where("title LIKE ? OR description LIKE ?", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
if err := db.Count(&total).Error; err != nil {
s.log.Errorw("Failed to count dramas", "error", err)
return nil, 0, err
}
offset := (query.Page - 1) * query.PageSize
err := db.Order("updated_at DESC").
Offset(offset).
Limit(query.PageSize).
Preload("Episodes.Storyboards", func(db *gorm.DB) *gorm.DB {
return db.Order("storyboards.storyboard_number ASC")
}).
Find(&dramas).Error
if err != nil {
s.log.Errorw("Failed to list dramas", "error", err)
return nil, 0, err
}
// 统计每个剧本的每个剧集的时长(基于场景时长之和)
for i := range dramas {
for j := range dramas[i].Episodes {
totalDuration := 0
for _, scene := range dramas[i].Episodes[j].Storyboards {
totalDuration += scene.Duration
}
// 更新剧集时长(秒转分钟,向上取整)
durationMinutes := (totalDuration + 59) / 60
dramas[i].Episodes[j].Duration = durationMinutes
}
}
return dramas, total, nil
}
func (s *DramaService) UpdateDrama(dramaID string, req *UpdateDramaRequest) (*models.Drama, error) {
var drama models.Drama
if err := s.db.Where("id = ? ", dramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("drama not found")
}
return nil, err
}
updates := make(map[string]interface{})
if req.Title != "" {
updates["title"] = req.Title
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Genre != "" {
updates["genre"] = req.Genre
}
if req.Style != "" {
updates["style"] = req.Style
}
if req.Tags != "" {
updates["tags"] = req.Tags
}
if req.Status != "" {
updates["status"] = req.Status
}
updates["updated_at"] = time.Now()
if err := s.db.Model(&drama).Updates(updates).Error; err != nil {
s.log.Errorw("Failed to update drama", "error", err)
return nil, err
}
s.log.Infow("Drama updated", "drama_id", dramaID)
return &drama, nil
}
func (s *DramaService) DeleteDrama(dramaID string) error {
result := s.db.Where("id = ? ", dramaID).Delete(&models.Drama{})
if result.Error != nil {
s.log.Errorw("Failed to delete drama", "error", result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("drama not found")
}
s.log.Infow("Drama deleted", "drama_id", dramaID)
return nil
}
func (s *DramaService) GetDramaStats() (map[string]interface{}, error) {
var total int64
var byStatus []struct {
Status string
Count int64
}
if err := s.db.Model(&models.Drama{}).Count(&total).Error; err != nil {
return nil, err
}
if err := s.db.Model(&models.Drama{}).
Select("status, count(*) as count").
Group("status").
Scan(&byStatus).Error; err != nil {
return nil, err
}
stats := map[string]interface{}{
"total": total,
"by_status": byStatus,
}
return stats, nil
}
type SaveOutlineRequest struct {
Title string `json:"title" binding:"required"`
Summary string `json:"summary" binding:"required"`
Genre string `json:"genre"`
Tags []string `json:"tags"`
}
type SaveCharactersRequest struct {
Characters []models.Character `json:"characters" binding:"required"`
EpisodeID *uint `json:"episode_id"` // 可选:如果提供则关联到指定章节
}
type SaveProgressRequest struct {
CurrentStep string `json:"current_step" binding:"required"`
StepData map[string]interface{} `json:"step_data"`
}
type SaveEpisodesRequest struct {
Episodes []models.Episode `json:"episodes" binding:"required"`
}
func (s *DramaService) SaveOutline(dramaID string, req *SaveOutlineRequest) error {
var drama models.Drama
if err := s.db.Where("id = ? ", dramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("drama not found")
}
return err
}
updates := map[string]interface{}{
"title": req.Title,
"description": req.Summary,
"updated_at": time.Now(),
}
if req.Genre != "" {
updates["genre"] = req.Genre
}
if len(req.Tags) > 0 {
tagsJSON, err := json.Marshal(req.Tags)
if err != nil {
s.log.Errorw("Failed to marshal tags", "error", err)
return err
}
updates["tags"] = tagsJSON
}
if err := s.db.Model(&drama).Updates(updates).Error; err != nil {
s.log.Errorw("Failed to save outline", "error", err)
return err
}
s.log.Infow("Outline saved", "drama_id", dramaID)
return nil
}
func (s *DramaService) GetCharacters(dramaID string, episodeID *string) ([]models.Character, error) {
var drama models.Drama
if err := s.db.Where("id = ? ", dramaID).First(&drama).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("drama not found")
}
return nil, err
}
var characters []models.Character
// 如果指定了episodeID,只获取该章节关联的角色
if episodeID != nil {
var episode models.Episode
if err := s.db.Preload("Characters").Where("id = ? AND drama_id = ?", *episodeID, dramaID).First(&episode).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("episode not found")
}
return nil, err
}
characters = episode.Characters
} else {
// 如果没有指定episodeID,获取项目的所有角色
if err := s.db.Where("drama_id = ?", dramaID).Find(&characters).Error; err != nil {
s.log.Errorw("Failed to get characters", "error", err)
return nil, err
}
}
// 查询每个角色的图片生成任务状态
for i := range characters {
// 查询该角色最新的图片生成任务
var imageGen models.ImageGeneration
err := s.db.Where("character
gitextract_vm6sptpc/
├── .dockerignore
├── .gitignore
├── DOCKER_HOST_ACCESS.md
├── Dockerfile
├── LICENSE
├── MIGRATE_README.md
├── README-CN.md
├── README-JA.md
├── README.md
├── api/
│ ├── handlers/
│ │ ├── ai_config.go
│ │ ├── asset.go
│ │ ├── audio_extraction.go
│ │ ├── character_batch.go
│ │ ├── character_library.go
│ │ ├── character_library_gen.go
│ │ ├── drama.go
│ │ ├── frame_prompt.go
│ │ ├── frame_prompt_query.go
│ │ ├── image_generation.go
│ │ ├── prop.go
│ │ ├── scene.go
│ │ ├── script_generation.go
│ │ ├── settings.go
│ │ ├── storyboard.go
│ │ ├── task.go
│ │ ├── upload.go
│ │ ├── video_generation.go
│ │ └── video_merge.go
│ ├── middlewares/
│ │ ├── cors.go
│ │ ├── logger.go
│ │ └── ratelimit.go
│ └── routes/
│ └── routes.go
├── application/
│ └── services/
│ ├── ai_service.go
│ ├── asset_duration_update.go
│ ├── asset_service.go
│ ├── audio_extraction_service.go
│ ├── character_library_service.go
│ ├── data_migration_service.go
│ ├── drama_service.go
│ ├── frame_prompt_helper.go
│ ├── frame_prompt_service.go
│ ├── image_generation_service.go
│ ├── prompt_i18n.go
│ ├── prop_service.go
│ ├── resource_transfer_service.go
│ ├── script_generation_service.go
│ ├── storyboard_composition_service.go
│ ├── storyboard_service.go
│ ├── storyboard_update_full.go
│ ├── task_service.go
│ ├── upload_service.go
│ ├── video_generation_service.go
│ └── video_merge_service.go
├── cmd/
│ └── migrate/
│ └── main.go
├── configs/
│ └── config.example.yaml
├── docker-compose.yml
├── docs/
│ └── DATA_MIGRATION.md
├── domain/
│ └── models/
│ ├── ai_config.go
│ ├── asset.go
│ ├── character_library.go
│ ├── drama.go
│ ├── frame_prompt.go
│ ├── image_generation.go
│ ├── task.go
│ ├── timeline.go
│ ├── video_generation.go
│ └── video_merge.go
├── go.mod
├── go.sum
├── infrastructure/
│ ├── database/
│ │ ├── custom_logger.go
│ │ └── database.go
│ ├── external/
│ │ └── ffmpeg/
│ │ └── ffmpeg.go
│ ├── scheduler/
│ │ └── resource_transfer_scheduler.go
│ └── storage/
│ └── local_storage.go
├── main.go
├── migrations/
│ ├── 20260126_add_local_path.sql
│ └── init.sql
├── pkg/
│ ├── ai/
│ │ ├── client.go
│ │ ├── gemini_client.go
│ │ └── openai_client.go
│ ├── config/
│ │ └── config.go
│ ├── image/
│ │ ├── gemini_image_client.go
│ │ ├── image_client.go
│ │ ├── openai_image_client.go
│ │ └── volcengine_image_client.go
│ ├── logger/
│ │ └── logger.go
│ ├── response/
│ │ └── response.go
│ ├── utils/
│ │ ├── image_utils.go
│ │ ├── json_parser.go
│ │ └── json_parser_test.go
│ └── video/
│ ├── chatfire_client.go
│ ├── minimax_client.go
│ ├── openai_sora_client.go
│ ├── video_client.go
│ └── volces_ark_client.go
└── web/
├── .gitignore
├── index.html
├── nginx.conf
├── package.json
├── public/
│ └── ffmpeg/
│ ├── ffmpeg-core.js
│ └── ffmpeg-core.wasm
├── src/
│ ├── App.vue
│ ├── api/
│ │ ├── ai.ts
│ │ ├── asset.ts
│ │ ├── audio.ts
│ │ ├── character-library.ts
│ │ ├── drama.ts
│ │ ├── frame.ts
│ │ ├── generation.ts
│ │ ├── image.ts
│ │ ├── prop.ts
│ │ ├── settings.ts
│ │ ├── task.ts
│ │ ├── video.ts
│ │ └── videoMerge.ts
│ ├── assets/
│ │ └── styles/
│ │ ├── element/
│ │ │ └── index.scss
│ │ └── main.css
│ ├── components/
│ │ ├── LanguageSwitcher.vue
│ │ ├── common/
│ │ │ ├── AIConfigDialog.vue
│ │ │ ├── ActionButton.vue
│ │ │ ├── AppHeader.vue
│ │ │ ├── AppLayout.vue
│ │ │ ├── BaseCard.vue
│ │ │ ├── CreateDramaDialog.vue
│ │ │ ├── EmptyState.vue
│ │ │ ├── ImageCropDialog.vue
│ │ │ ├── ImagePreview.vue
│ │ │ ├── PageHeader.vue
│ │ │ ├── ProjectCard.vue
│ │ │ ├── StatCard.vue
│ │ │ ├── ThemeToggle.vue
│ │ │ └── index.ts
│ │ └── editor/
│ │ ├── GridImageEditor.vue
│ │ ├── StoryboardEditor.vue
│ │ └── VideoTimelineEditor.vue
│ ├── locales/
│ │ ├── en-US.ts
│ │ ├── index.ts
│ │ └── zh-CN.ts
│ ├── main.ts
│ ├── router/
│ │ └── index.ts
│ ├── stores/
│ │ └── episode.ts
│ ├── types/
│ │ ├── ai.ts
│ │ ├── asset.ts
│ │ ├── drama.ts
│ │ ├── generation.ts
│ │ ├── image.ts
│ │ ├── prop.ts
│ │ ├── timeline.ts
│ │ ├── user.ts
│ │ └── video.ts
│ ├── utils/
│ │ ├── ffmpeg.ts
│ │ ├── image.ts
│ │ ├── request.ts
│ │ └── videoMerger.ts
│ └── views/
│ ├── dashboard/
│ │ └── Dashboard.vue
│ ├── drama/
│ │ ├── DramaCreate.vue
│ │ ├── DramaList.vue
│ │ ├── DramaManagement.vue
│ │ ├── DramaWorkflow.vue
│ │ ├── EpisodeWorkflow.vue
│ │ ├── ProfessionalEditor.vue
│ │ └── components/
│ │ └── UploadScriptDialog.vue
│ ├── editor/
│ │ └── TimelineEditor.vue
│ ├── generation/
│ │ ├── ImageGeneration.vue
│ │ ├── VideoGeneration.vue
│ │ └── components/
│ │ ├── GenerateImageDialog.vue
│ │ ├── GenerateVideoDialog.vue
│ │ ├── ImageDetailDialog.vue
│ │ └── VideoDetailDialog.vue
│ ├── script/
│ │ └── ScriptEdit.vue
│ ├── settings/
│ │ ├── AIConfig.vue
│ │ ├── SystemSettings.vue
│ │ └── components/
│ │ └── ConfigList.vue
│ ├── storyboard/
│ │ └── StoryboardEdit.vue
│ └── workflow/
│ ├── CharacterExtraction.vue
│ ├── CharacterImages.vue
│ ├── DramaSettings.vue
│ ├── SceneImages.vue
│ └── StoryboardGeneration.vue
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
SYMBOL INDEX (1198 symbols across 108 files)
FILE: api/handlers/ai_config.go
type AIConfigHandler (line 14) | type AIConfigHandler struct
method CreateConfig (line 26) | func (h *AIConfigHandler) CreateConfig(c *gin.Context) {
method GetConfig (line 42) | func (h *AIConfigHandler) GetConfig(c *gin.Context) {
method ListConfigs (line 63) | func (h *AIConfigHandler) ListConfigs(c *gin.Context) {
method UpdateConfig (line 76) | func (h *AIConfigHandler) UpdateConfig(c *gin.Context) {
method DeleteConfig (line 103) | func (h *AIConfigHandler) DeleteConfig(c *gin.Context) {
method TestConnection (line 123) | func (h *AIConfigHandler) TestConnection(c *gin.Context) {
function NewAIConfigHandler (line 19) | func NewAIConfigHandler(db *gorm.DB, cfg *config.Config, log *logger.Log...
FILE: api/handlers/asset.go
type AssetHandler (line 16) | type AssetHandler struct
method CreateAsset (line 28) | func (h *AssetHandler) CreateAsset(c *gin.Context) {
method UpdateAsset (line 46) | func (h *AssetHandler) UpdateAsset(c *gin.Context) {
method GetAsset (line 70) | func (h *AssetHandler) GetAsset(c *gin.Context) {
method ListAssets (line 87) | func (h *AssetHandler) ListAssets(c *gin.Context) {
method DeleteAsset (line 169) | func (h *AssetHandler) DeleteAsset(c *gin.Context) {
method ImportFromImageGen (line 186) | func (h *AssetHandler) ImportFromImageGen(c *gin.Context) {
method ImportFromVideoGen (line 204) | func (h *AssetHandler) ImportFromVideoGen(c *gin.Context) {
function NewAssetHandler (line 21) | func NewAssetHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger...
FILE: api/handlers/audio_extraction.go
type AudioExtractionHandler (line 11) | type AudioExtractionHandler struct
method ExtractAudio (line 34) | func (h *AudioExtractionHandler) ExtractAudio(c *gin.Context) {
method BatchExtractAudio (line 67) | func (h *AudioExtractionHandler) BatchExtractAudio(c *gin.Context) {
function NewAudioExtractionHandler (line 17) | func NewAudioExtractionHandler(log *logger.Logger, dataDir string) *Audi...
type BatchExtractAudioRequest (line 54) | type BatchExtractAudioRequest struct
FILE: api/handlers/character_batch.go
method BatchGenerateCharacterImages (line 9) | func (h *CharacterLibraryHandler) BatchGenerateCharacterImages(c *gin.Co...
FILE: api/handlers/character_library.go
type CharacterLibraryHandler (line 15) | type CharacterLibraryHandler struct
method ListLibraryItems (line 30) | func (h *CharacterLibraryHandler) ListLibraryItems(c *gin.Context) {
method CreateLibraryItem (line 56) | func (h *CharacterLibraryHandler) CreateLibraryItem(c *gin.Context) {
method GetLibraryItem (line 75) | func (h *CharacterLibraryHandler) GetLibraryItem(c *gin.Context) {
method DeleteLibraryItem (line 94) | func (h *CharacterLibraryHandler) DeleteLibraryItem(c *gin.Context) {
method UploadCharacterImage (line 112) | func (h *CharacterLibraryHandler) UploadCharacterImage(c *gin.Context) {
method ApplyLibraryItemToCharacter (line 146) | func (h *CharacterLibraryHandler) ApplyLibraryItemToCharacter(c *gin.C...
method AddCharacterToLibrary (line 181) | func (h *CharacterLibraryHandler) AddCharacterToLibrary(c *gin.Context) {
method UpdateCharacter (line 217) | func (h *CharacterLibraryHandler) UpdateCharacter(c *gin.Context) {
method DeleteCharacter (line 245) | func (h *CharacterLibraryHandler) DeleteCharacter(c *gin.Context) {
method ExtractCharacters (line 272) | func (h *CharacterLibraryHandler) ExtractCharacters(c *gin.Context) {
function NewCharacterLibraryHandler (line 21) | func NewCharacterLibraryHandler(db *gorm.DB, cfg *config.Config, log *lo...
FILE: api/handlers/character_library_gen.go
method GenerateCharacterImage (line 9) | func (h *CharacterLibraryHandler) GenerateCharacterImage(c *gin.Context) {
FILE: api/handlers/drama.go
type DramaHandler (line 15) | type DramaHandler struct
method CreateDrama (line 31) | func (h *DramaHandler) CreateDrama(c *gin.Context) {
method GetDrama (line 48) | func (h *DramaHandler) GetDrama(c *gin.Context) {
method ListDramas (line 65) | func (h *DramaHandler) ListDramas(c *gin.Context) {
method UpdateDrama (line 89) | func (h *DramaHandler) UpdateDrama(c *gin.Context) {
method DeleteDrama (line 112) | func (h *DramaHandler) DeleteDrama(c *gin.Context) {
method GetDramaStats (line 128) | func (h *DramaHandler) GetDramaStats(c *gin.Context) {
method SaveOutline (line 139) | func (h *DramaHandler) SaveOutline(c *gin.Context) {
method GetCharacters (line 161) | func (h *DramaHandler) GetCharacters(c *gin.Context) {
method SaveCharacters (line 188) | func (h *DramaHandler) SaveCharacters(c *gin.Context) {
method SaveEpisodes (line 248) | func (h *DramaHandler) SaveEpisodes(c *gin.Context) {
method SaveProgress (line 270) | func (h *DramaHandler) SaveProgress(c *gin.Context) {
method FinalizeEpisode (line 293) | func (h *DramaHandler) FinalizeEpisode(c *gin.Context) {
method DownloadEpisodeVideo (line 323) | func (h *DramaHandler) DownloadEpisodeVideo(c *gin.Context) {
function NewDramaHandler (line 22) | func NewDramaHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger...
FILE: api/handlers/frame_prompt.go
type FramePromptHandler (line 11) | type FramePromptHandler struct
method GenerateFramePrompt (line 26) | func (h *FramePromptHandler) GenerateFramePrompt(c *gin.Context) {
function NewFramePromptHandler (line 17) | func NewFramePromptHandler(framePromptService *services.FramePromptServi...
FILE: api/handlers/frame_prompt_query.go
function GetStoryboardFramePrompts (line 13) | func GetStoryboardFramePrompts(db *gorm.DB, log *logger.Logger) gin.Hand...
FILE: api/handlers/image_generation.go
type ImageGenerationHandler (line 16) | type ImageGenerationHandler struct
method GenerateImage (line 34) | func (h *ImageGenerationHandler) GenerateImage(c *gin.Context) {
method GenerateImagesForScene (line 52) | func (h *ImageGenerationHandler) GenerateImagesForScene(c *gin.Context) {
method GetBackgroundsForEpisode (line 66) | func (h *ImageGenerationHandler) GetBackgroundsForEpisode(c *gin.Conte...
method ExtractBackgroundsForEpisode (line 80) | func (h *ImageGenerationHandler) ExtractBackgroundsForEpisode(c *gin.C...
method BatchGenerateForEpisode (line 117) | func (h *ImageGenerationHandler) BatchGenerateForEpisode(c *gin.Contex...
method GetImageGeneration (line 131) | func (h *ImageGenerationHandler) GetImageGeneration(c *gin.Context) {
method ListImageGenerations (line 148) | func (h *ImageGenerationHandler) ListImageGenerations(c *gin.Context) {
method DeleteImageGeneration (line 197) | func (h *ImageGenerationHandler) DeleteImageGeneration(c *gin.Context) {
method UploadImage (line 215) | func (h *ImageGenerationHandler) UploadImage(c *gin.Context) {
function NewImageGenerationHandler (line 24) | func NewImageGenerationHandler(db *gorm.DB, cfg *config.Config, log *log...
FILE: api/handlers/prop.go
type PropHandler (line 15) | type PropHandler struct
method ListProps (line 28) | func (h *PropHandler) ListProps(c *gin.Context) {
method CreateProp (line 51) | func (h *PropHandler) CreateProp(c *gin.Context) {
method UpdateProp (line 67) | func (h *PropHandler) UpdateProp(c *gin.Context) {
method DeleteProp (line 90) | func (h *PropHandler) DeleteProp(c *gin.Context) {
method ExtractProps (line 107) | func (h *PropHandler) ExtractProps(c *gin.Context) {
method GenerateImage (line 125) | func (h *PropHandler) GenerateImage(c *gin.Context) {
method AssociateProps (line 144) | func (h *PropHandler) AssociateProps(c *gin.Context) {
function NewPropHandler (line 20) | func NewPropHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger,...
FILE: api/handlers/scene.go
type SceneHandler (line 11) | type SceneHandler struct
method GetStoryboardsForEpisode (line 23) | func (h *SceneHandler) GetStoryboardsForEpisode(c *gin.Context) {
method UpdateScene (line 39) | func (h *SceneHandler) UpdateScene(c *gin.Context) {
method GenerateSceneImage (line 57) | func (h *SceneHandler) GenerateSceneImage(c *gin.Context) {
method UpdateScenePrompt (line 77) | func (h *SceneHandler) UpdateScenePrompt(c *gin.Context) {
method DeleteScene (line 99) | func (h *SceneHandler) DeleteScene(c *gin.Context) {
method CreateScene (line 115) | func (h *SceneHandler) CreateScene(c *gin.Context) {
function NewSceneHandler (line 16) | func NewSceneHandler(db *gorm.DB, log *logger.Logger, imageGenService *s...
FILE: api/handlers/script_generation.go
type ScriptGenerationHandler (line 12) | type ScriptGenerationHandler struct
method GenerateCharacters (line 26) | func (h *ScriptGenerationHandler) GenerateCharacters(c *gin.Context) {
function NewScriptGenerationHandler (line 18) | func NewScriptGenerationHandler(db *gorm.DB, cfg *config.Config, log *lo...
FILE: api/handlers/settings.go
type SettingsHandler (line 11) | type SettingsHandler struct
method GetLanguage (line 24) | func (h *SettingsHandler) GetLanguage(c *gin.Context) {
method UpdateLanguage (line 36) | func (h *SettingsHandler) UpdateLanguage(c *gin.Context) {
function NewSettingsHandler (line 16) | func NewSettingsHandler(cfg *config.Config, log *logger.Logger) *Setting...
FILE: api/handlers/storyboard.go
type StoryboardHandler (line 14) | type StoryboardHandler struct
method GenerateStoryboard (line 29) | func (h *StoryboardHandler) GenerateStoryboard(c *gin.Context) {
method UpdateStoryboard (line 58) | func (h *StoryboardHandler) UpdateStoryboard(c *gin.Context) {
method CreateStoryboard (line 79) | func (h *StoryboardHandler) CreateStoryboard(c *gin.Context) {
method DeleteStoryboard (line 97) | func (h *StoryboardHandler) DeleteStoryboard(c *gin.Context) {
function NewStoryboardHandler (line 20) | func NewStoryboardHandler(db *gorm.DB, cfg *config.Config, log *logger.L...
FILE: api/handlers/task.go
type TaskHandler (line 11) | type TaskHandler struct
method GetTaskStatus (line 24) | func (h *TaskHandler) GetTaskStatus(c *gin.Context) {
method GetResourceTasks (line 42) | func (h *TaskHandler) GetResourceTasks(c *gin.Context) {
function NewTaskHandler (line 16) | func NewTaskHandler(db *gorm.DB, log *logger.Logger) *TaskHandler {
FILE: api/handlers/upload.go
type UploadHandler (line 11) | type UploadHandler struct
method UploadImage (line 31) | func (h *UploadHandler) UploadImage(c *gin.Context) {
method UploadCharacterImage (line 83) | func (h *UploadHandler) UploadCharacterImage(c *gin.Context) {
function NewUploadHandler (line 17) | func NewUploadHandler(cfg *config.Config, log *logger.Logger, characterL...
FILE: api/handlers/video_generation.go
type VideoGenerationHandler (line 14) | type VideoGenerationHandler struct
method GenerateVideo (line 26) | func (h *VideoGenerationHandler) GenerateVideo(c *gin.Context) {
method GenerateVideoFromImage (line 44) | func (h *VideoGenerationHandler) GenerateVideoFromImage(c *gin.Context) {
method BatchGenerateForEpisode (line 62) | func (h *VideoGenerationHandler) BatchGenerateForEpisode(c *gin.Contex...
method GetVideoGeneration (line 76) | func (h *VideoGenerationHandler) GetVideoGeneration(c *gin.Context) {
method ListVideoGenerations (line 93) | func (h *VideoGenerationHandler) ListVideoGenerations(c *gin.Context) {
method DeleteVideoGeneration (line 134) | func (h *VideoGenerationHandler) DeleteVideoGeneration(c *gin.Context) {
function NewVideoGenerationHandler (line 19) | func NewVideoGenerationHandler(db *gorm.DB, transferService *services.Re...
FILE: api/handlers/video_merge.go
type VideoMergeHandler (line 13) | type VideoMergeHandler struct
method MergeVideos (line 25) | func (h *VideoMergeHandler) MergeVideos(c *gin.Context) {
method GetMerge (line 45) | func (h *VideoMergeHandler) GetMerge(c *gin.Context) {
method ListMerges (line 63) | func (h *VideoMergeHandler) ListMerges(c *gin.Context) {
method DeleteMerge (line 89) | func (h *VideoMergeHandler) DeleteMerge(c *gin.Context) {
function NewVideoMergeHandler (line 18) | func NewVideoMergeHandler(db *gorm.DB, transferService *services2.Resour...
FILE: api/middlewares/cors.go
function CORSMiddleware (line 7) | func CORSMiddleware(allowedOrigins []string) gin.HandlerFunc {
FILE: api/middlewares/logger.go
function LoggerMiddleware (line 10) | func LoggerMiddleware(log *logger.Logger) gin.HandlerFunc {
FILE: api/middlewares/ratelimit.go
type rateLimiter (line 11) | type rateLimiter struct
function RateLimitMiddleware (line 24) | func RateLimitMiddleware() gin.HandlerFunc {
FILE: api/routes/routes.go
function SetupRouter (line 14) | func SetupRouter(cfg *config.Config, db *gorm.DB, log *logger.Logger, lo...
FILE: application/services/ai_service.go
type AIService (line 13) | type AIService struct
method CreateConfig (line 61) | func (s *AIService) CreateConfig(req *CreateAIConfigRequest) (*models....
method GetConfig (line 137) | func (s *AIService) GetConfig(configID uint) (*models.AIServiceConfig,...
method ListConfigs (line 149) | func (s *AIService) ListConfigs(serviceType string) ([]models.AIServic...
method UpdateConfig (line 166) | func (s *AIService) UpdateConfig(configID uint, req *UpdateAIConfigReq...
method DeleteConfig (line 254) | func (s *AIService) DeleteConfig(configID uint) error {
method TestConnection (line 270) | func (s *AIService) TestConnection(req *TestConnectionRequest) error {
method GetDefaultConfig (line 318) | func (s *AIService) GetDefaultConfig(serviceType string) (*models.AISe...
method GetConfigForModel (line 336) | func (s *AIService) GetConfigForModel(serviceType string, modelName st...
method GetAIClient (line 358) | func (s *AIService) GetAIClient(serviceType string) (ai.AIClient, erro...
method GetAIClientForModel (line 392) | func (s *AIService) GetAIClientForModel(serviceType string, modelName ...
method GenerateText (line 419) | func (s *AIService) GenerateText(prompt string, systemPrompt string, o...
method GenerateImage (line 428) | func (s *AIService) GenerateImage(prompt string, size string, n int) (...
function NewAIService (line 18) | func NewAIService(db *gorm.DB, log *logger.Logger) *AIService {
type CreateAIConfigRequest (line 25) | type CreateAIConfigRequest struct
type UpdateAIConfigRequest (line 39) | type UpdateAIConfigRequest struct
type TestConnectionRequest (line 53) | type TestConnectionRequest struct
FILE: application/services/asset_duration_update.go
method UpdateAssetDurationFromFile (line 11) | func (s *AssetService) UpdateAssetDurationFromFile(assetID uint, localFi...
method UpdateAssetDurationFromURL (line 44) | func (s *AssetService) UpdateAssetDurationFromURL(assetID uint, localSto...
FILE: application/services/asset_service.go
type AssetService (line 14) | type AssetService struct
method CreateAsset (line 70) | func (s *AssetService) CreateAsset(req *CreateAssetRequest) (*models.A...
method UpdateAsset (line 113) | func (s *AssetService) UpdateAsset(assetID uint, req *UpdateAssetReque...
method GetAsset (line 149) | func (s *AssetService) GetAsset(assetID uint) (*models.Asset, error) {
method ListAssets (line 160) | func (s *AssetService) ListAssets(req *ListAssetsRequest) ([]models.As...
method DeleteAsset (line 209) | func (s *AssetService) DeleteAsset(assetID uint) error {
method ImportFromImageGen (line 220) | func (s *AssetService) ImportFromImageGen(imageGenID uint) (*models.As...
method ImportFromVideoGen (line 248) | func (s *AssetService) ImportFromVideoGen(videoGenID uint) (*models.As...
function NewAssetService (line 20) | func NewAssetService(db *gorm.DB, log *logger.Logger) *AssetService {
type CreateAssetRequest (line 28) | type CreateAssetRequest struct
type UpdateAssetRequest (line 48) | type UpdateAssetRequest struct
type ListAssetsRequest (line 57) | type ListAssetsRequest struct
FILE: application/services/audio_extraction_service.go
type AudioExtractionService (line 12) | type AudioExtractionService struct
method ExtractAudio (line 34) | func (s *AudioExtractionService) ExtractAudio(videoURL string, dataDir...
method BatchExtractAudio (line 77) | func (s *AudioExtractionService) BatchExtractAudio(videoURLs []string,...
function NewAudioExtractionService (line 17) | func NewAudioExtractionService(log *logger.Logger) *AudioExtractionServi...
type ExtractAudioRequest (line 24) | type ExtractAudioRequest struct
type ExtractAudioResponse (line 28) | type ExtractAudioResponse struct
FILE: application/services/character_library_service.go
type CharacterLibraryService (line 16) | type CharacterLibraryService struct
method ListLibraryItems (line 55) | func (s *CharacterLibraryService) ListLibraryItems(query *CharacterLib...
method CreateLibraryItem (line 96) | func (s *CharacterLibraryService) CreateLibraryItem(req *CreateLibrary...
method GetLibraryItem (line 122) | func (s *CharacterLibraryService) GetLibraryItem(itemID string) (*mode...
method DeleteLibraryItem (line 138) | func (s *CharacterLibraryService) DeleteLibraryItem(itemID string) err...
method ApplyLibraryItemToCharacter (line 155) | func (s *CharacterLibraryService) ApplyLibraryItemToCharacter(characte...
method UploadCharacterImage (line 203) | func (s *CharacterLibraryService) UploadCharacterImage(characterID str...
method AddCharacterToLibrary (line 233) | func (s *CharacterLibraryService) AddCharacterToLibrary(characterID st...
method DeleteCharacter (line 276) | func (s *CharacterLibraryService) DeleteCharacter(characterID uint) er...
method GenerateCharacterImage (line 306) | func (s *CharacterLibraryService) GenerateCharacterImage(characterID s...
method waitAndUpdateCharacterImage (line 370) | func (s *CharacterLibraryService) waitAndUpdateCharacterImage(characte...
method UpdateCharacter (line 416) | func (s *CharacterLibraryService) UpdateCharacter(characterID string, ...
method BatchGenerateCharacterImages (line 475) | func (s *CharacterLibraryService) BatchGenerateCharacterImages(charact...
method ExtractCharactersFromScript (line 503) | func (s *CharacterLibraryService) ExtractCharactersFromScript(episodeI...
method processCharacterExtraction (line 523) | func (s *CharacterLibraryService) processCharacterExtraction(taskID st...
function NewCharacterLibraryService (line 25) | func NewCharacterLibraryService(db *gorm.DB, log *logger.Logger, cfg *co...
type CreateLibraryItemRequest (line 36) | type CreateLibraryItemRequest struct
type CharacterLibraryQuery (line 46) | type CharacterLibraryQuery struct
type UpdateCharacterRequest (line 405) | type UpdateCharacterRequest struct
FILE: application/services/data_migration_service.go
type DataMigrationService (line 18) | type DataMigrationService struct
method MigrateLocalPaths (line 35) | func (s *DataMigrationService) MigrateLocalPaths() error {
method ensureStorageDirectories (line 115) | func (s *DataMigrationService) ensureStorageDirectories() error {
method migrateAssets (line 133) | func (s *DataMigrationService) migrateAssets(stats *MigrationStats) er...
method migrateCharacterLibraries (line 175) | func (s *DataMigrationService) migrateCharacterLibraries(stats *Migrat...
method migrateImageGenerations (line 211) | func (s *DataMigrationService) migrateImageGenerations(stats *Migratio...
method migrateScenes (line 258) | func (s *DataMigrationService) migrateScenes(stats *MigrationStats) er...
method migrateCharacters (line 297) | func (s *DataMigrationService) migrateCharacters(stats *MigrationStats...
method migrateVideoGenerations (line 336) | func (s *DataMigrationService) migrateVideoGenerations(stats *Migratio...
method downloadOrGetCached (line 375) | func (s *DataMigrationService) downloadOrGetCached(url, subDir, prefix...
method downloadAndSaveImage (line 405) | func (s *DataMigrationService) downloadAndSaveImage(imageURL, subDir, ...
method downloadAndSaveVideo (line 434) | func (s *DataMigrationService) downloadAndSaveVideo(videoURL, subDir, ...
method extractFileExtension (line 466) | func (s *DataMigrationService) extractFileExtension(url string) string {
method downloadFile (line 496) | func (s *DataMigrationService) downloadFile(url, filepath string) error {
function NewDataMigrationService (line 25) | func NewDataMigrationService(db *gorm.DB, log *logger.Logger) *DataMigra...
type MigrationStats (line 99) | type MigrationStats struct
FILE: application/services/drama_service.go
type DramaService (line 16) | type DramaService struct
method CreateDrama (line 55) | func (s *DramaService) CreateDrama(req *CreateDramaRequest) (*models.D...
method GetDrama (line 81) | func (s *DramaService) GetDrama(dramaID string) (*models.Drama, error) {
method ListDramas (line 206) | func (s *DramaService) ListDramas(query *DramaListQuery) ([]models.Dra...
method UpdateDrama (line 259) | func (s *DramaService) UpdateDrama(dramaID string, req *UpdateDramaReq...
method DeleteDrama (line 300) | func (s *DramaService) DeleteDrama(dramaID string) error {
method GetDramaStats (line 316) | func (s *DramaService) GetDramaStats() (map[string]interface{}, error) {
method SaveOutline (line 363) | func (s *DramaService) SaveOutline(dramaID string, req *SaveOutlineReq...
method GetCharacters (line 400) | func (s *DramaService) GetCharacters(dramaID string, episodeID *string...
method SaveCharacters (line 455) | func (s *DramaService) SaveCharacters(dramaID string, req *SaveCharact...
method SaveEpisodes (line 579) | func (s *DramaService) SaveEpisodes(dramaID string, req *SaveEpisodesR...
method SaveProgress (line 627) | func (s *DramaService) SaveProgress(dramaID string, req *SaveProgressR...
method addBaseURLToScenes (line 674) | func (s *DramaService) addBaseURLToScenes(drama *models.Drama) {
function NewDramaService (line 22) | func NewDramaService(db *gorm.DB, cfg *config.Config, log *logger.Logger...
type CreateDramaRequest (line 30) | type CreateDramaRequest struct
type UpdateDramaRequest (line 38) | type UpdateDramaRequest struct
type DramaListQuery (line 47) | type DramaListQuery struct
type SaveOutlineRequest (line 342) | type SaveOutlineRequest struct
type SaveCharactersRequest (line 349) | type SaveCharactersRequest struct
type SaveProgressRequest (line 354) | type SaveProgressRequest struct
type SaveEpisodesRequest (line 359) | type SaveEpisodesRequest struct
FILE: application/services/frame_prompt_helper.go
method parseFramePromptJSON (line 10) | func (s *FramePromptService) parseFramePromptJSON(aiResponse string) *Si...
FILE: application/services/frame_prompt_service.go
type FramePromptService (line 14) | type FramePromptService struct
method GenerateFramePrompt (line 74) | func (s *FramePromptService) GenerateFramePrompt(req GenerateFrameProm...
method processFramePromptGeneration (line 96) | func (s *FramePromptService) processFramePromptGeneration(taskID strin...
method saveFramePrompt (line 179) | func (s *FramePromptService) saveFramePrompt(storyboardID, frameType, ...
method generateFirstFrame (line 210) | func (s *FramePromptService) generateFirstFrame(sb models.Storyboard, ...
method generateKeyFrame (line 258) | func (s *FramePromptService) generateKeyFrame(sb models.Storyboard, sc...
method generateLastFrame (line 305) | func (s *FramePromptService) generateLastFrame(sb models.Storyboard, s...
method generatePanelFrames (line 352) | func (s *FramePromptService) generatePanelFrames(sb models.Storyboard,...
method generateActionSequence (line 382) | func (s *FramePromptService) generateActionSequence(sb models.Storyboa...
method buildStoryboardContext (line 445) | func (s *FramePromptService) buildStoryboardContext(sb models.Storyboa...
method buildFallbackPrompt (line 504) | func (s *FramePromptService) buildFallbackPrompt(sb models.Storyboard,...
function NewFramePromptService (line 24) | func NewFramePromptService(db *gorm.DB, cfg *config.Config, log *logger....
type FrameType (line 36) | type FrameType
constant FrameTypeFirst (line 39) | FrameTypeFirst FrameType = "first"
constant FrameTypeKey (line 40) | FrameTypeKey FrameType = "key"
constant FrameTypeLast (line 41) | FrameTypeLast FrameType = "last"
constant FrameTypePanel (line 42) | FrameTypePanel FrameType = "panel"
constant FrameTypeAction (line 43) | FrameTypeAction FrameType = "action"
type GenerateFramePromptRequest (line 47) | type GenerateFramePromptRequest struct
type FramePromptResponse (line 55) | type FramePromptResponse struct
type SingleFramePrompt (line 62) | type SingleFramePrompt struct
type MultiFramePrompt (line 68) | type MultiFramePrompt struct
function mustParseUint (line 203) | func mustParseUint(s string) uint64 {
FILE: application/services/image_generation_service.go
type ImageGenerationService (line 23) | type ImageGenerationService struct
method GetDB (line 66) | func (s *ImageGenerationService) GetDB() *gorm.DB {
method GenerateImage (line 94) | func (s *ImageGenerationService) GenerateImage(request *GenerateImageR...
method ProcessImageGeneration (line 158) | func (s *ImageGenerationService) ProcessImageGeneration(imageGenID uin...
method pollTaskStatus (line 311) | func (s *ImageGenerationService) pollTaskStatus(imageGenID uint, clien...
method completeImageGeneration (line 338) | func (s *ImageGenerationService) completeImageGeneration(imageGenID ui...
method updateImageGenError (line 466) | func (s *ImageGenerationService) updateImageGenError(imageGenID uint, ...
method getImageClient (line 488) | func (s *ImageGenerationService) getImageClient(provider string) (imag...
method getImageClientWithModel (line 531) | func (s *ImageGenerationService) getImageClientWithModel(provider stri...
method GetImageGeneration (line 588) | func (s *ImageGenerationService) GetImageGeneration(imageGenID uint) (...
method ListImageGenerations (line 596) | func (s *ImageGenerationService) ListImageGenerations(dramaID *uint, s...
method DeleteImageGeneration (line 633) | func (s *ImageGenerationService) DeleteImageGeneration(imageGenID uint...
method CreateImageFromUpload (line 654) | func (s *ImageGenerationService) CreateImageFromUpload(req *UploadImag...
method GenerateImagesForScene (line 698) | func (s *ImageGenerationService) GenerateImagesForScene(sceneID string...
method BatchGenerateImagesForEpisode (line 744) | func (s *ImageGenerationService) BatchGenerateImagesForEpisode(episode...
method GetScencesForEpisode (line 800) | func (s *ImageGenerationService) GetScencesForEpisode(episodeID string...
method ExtractBackgroundsForEpisode (line 816) | func (s *ImageGenerationService) ExtractBackgroundsForEpisode(episodeI...
method processBackgroundExtraction (line 842) | func (s *ImageGenerationService) processBackgroundExtraction(taskID st...
method extractBackgroundsFromScript (line 931) | func (s *ImageGenerationService) extractBackgroundsFromScript(scriptCo...
method extractBackgroundsWithAI (line 1088) | func (s *ImageGenerationService) extractBackgroundsWithAI(storyboards ...
method extractUniqueBackgrounds (line 1270) | func (s *ImageGenerationService) extractUniqueBackgrounds(scenes []mod...
method loadImageAsBase64 (line 1311) | func (s *ImageGenerationService) loadImageAsBase64(localPath string) (...
function truncateImageURL (line 35) | func truncateImageURL(url string) string {
function NewImageGenerationService (line 52) | func NewImageGenerationService(db *gorm.DB, cfg *config.Config, transfer...
type GenerateImageRequest (line 70) | type GenerateImageRequest struct
type UploadImageRequest (line 645) | type UploadImageRequest struct
type BackgroundInfo (line 734) | type BackgroundInfo struct
FILE: application/services/prompt_i18n.go
type PromptI18n (line 10) | type PromptI18n struct
method GetLanguage (line 20) | func (p *PromptI18n) GetLanguage() string {
method IsEnglish (line 29) | func (p *PromptI18n) IsEnglish() bool {
method GetStoryboardSystemPrompt (line 34) | func (p *PromptI18n) GetStoryboardSystemPrompt() string {
method GetSceneExtractionPrompt (line 147) | func (p *PromptI18n) GetSceneExtractionPrompt(style string) string {
method GetFirstFramePrompt (line 200) | func (p *PromptI18n) GetFirstFramePrompt(style string) string {
method GetKeyFramePrompt (line 240) | func (p *PromptI18n) GetKeyFramePrompt(style string) string {
method GetActionSequenceFramePrompt (line 280) | func (p *PromptI18n) GetActionSequenceFramePrompt(style string) string {
method GetLastFramePrompt (line 365) | func (p *PromptI18n) GetLastFramePrompt(style string) string {
method GetOutlineGenerationPrompt (line 405) | func (p *PromptI18n) GetOutlineGenerationPrompt() string {
method GetCharacterExtractionPrompt (line 448) | func (p *PromptI18n) GetCharacterExtractionPrompt(style string) string {
method GetPropExtractionPrompt (line 494) | func (p *PromptI18n) GetPropExtractionPrompt(style string) string {
method GetEpisodeScriptPrompt (line 545) | func (p *PromptI18n) GetEpisodeScriptPrompt() string {
method FormatUserPrompt (line 592) | func (p *PromptI18n) FormatUserPrompt(key string, args ...interface{})...
method GetStylePrompt (line 676) | func (p *PromptI18n) GetStylePrompt(style string) string {
method GetVideoConstraintPrompt (line 846) | func (p *PromptI18n) GetVideoConstraintPrompt(referenceMode string) st...
function NewPromptI18n (line 15) | func NewPromptI18n(cfg *config.Config) *PromptI18n {
FILE: application/services/prop_service.go
type PropService (line 16) | type PropService struct
method ListProps (line 39) | func (s *PropService) ListProps(dramaID uint) ([]models.Prop, error) {
method CreateProp (line 48) | func (s *PropService) CreateProp(prop *models.Prop) error {
method UpdateProp (line 53) | func (s *PropService) UpdateProp(id uint, updates map[string]interface...
method DeleteProp (line 58) | func (s *PropService) DeleteProp(id uint) error {
method ExtractPropsFromScript (line 63) | func (s *PropService) ExtractPropsFromScript(episodeID uint) (string, ...
method processPropExtraction (line 79) | func (s *PropService) processPropExtraction(taskID string, episode mod...
method GeneratePropImage (line 148) | func (s *PropService) GeneratePropImage(propID uint) (string, error) {
method processPropImageGeneration (line 169) | func (s *PropService) processPropImageGeneration(taskID string, prop m...
method AssociatePropsWithStoryboard (line 232) | func (s *PropService) AssociatePropsWithStoryboard(storyboardID uint, ...
function NewPropService (line 26) | func NewPropService(db *gorm.DB, aiService *AIService, taskService *Task...
FILE: application/services/resource_transfer_service.go
type ResourceTransferService (line 8) | type ResourceTransferService struct
function NewResourceTransferService (line 13) | func NewResourceTransferService(db *gorm.DB, log *logger.Logger) *Resour...
FILE: application/services/script_generation_service.go
type ScriptGenerationService (line 15) | type ScriptGenerationService struct
method GenerateCharacters (line 44) | func (s *ScriptGenerationService) GenerateCharacters(req *GenerateChar...
method processCharacterGeneration (line 65) | func (s *ScriptGenerationService) processCharacterGeneration(taskID st...
function NewScriptGenerationService (line 24) | func NewScriptGenerationService(db *gorm.DB, cfg *config.Config, log *lo...
type GenerateCharactersRequest (line 35) | type GenerateCharactersRequest struct
function minInt (line 197) | func minInt(a, b int) int {
FILE: application/services/storyboard_composition_service.go
type StoryboardCompositionService (line 12) | type StoryboardCompositionService struct
method GetScenesForEpisode (line 72) | func (s *StoryboardCompositionService) GetScenesForEpisode(episodeID s...
method UpdateScene (line 282) | func (s *StoryboardCompositionService) UpdateScene(sceneID string, req...
method GenerateSceneImage (line 356) | func (s *StoryboardCompositionService) GenerateSceneImage(req *Generat...
method UpdateScenePrompt (line 418) | func (s *StoryboardCompositionService) UpdateScenePrompt(sceneID strin...
method UpdateSceneInfo (line 445) | func (s *StoryboardCompositionService) UpdateSceneInfo(sceneID string,...
method DeleteScene (line 484) | func (s *StoryboardCompositionService) DeleteScene(sceneID string) err...
method CreateScene (line 520) | func (s *StoryboardCompositionService) CreateScene(req *CreateSceneReq...
function NewStoryboardCompositionService (line 18) | func NewStoryboardCompositionService(db *gorm.DB, log *logger.Logger, im...
type SceneCharacterInfo (line 26) | type SceneCharacterInfo struct
type SceneBackgroundInfo (line 33) | type SceneBackgroundInfo struct
type SceneCompositionInfo (line 42) | type SceneCompositionInfo struct
type UpdateSceneRequest (line 267) | type UpdateSceneRequest struct
type GenerateSceneImageRequest (line 350) | type GenerateSceneImageRequest struct
type UpdateScenePromptRequest (line 414) | type UpdateScenePromptRequest struct
type UpdateSceneInfoRequest (line 436) | type UpdateSceneInfoRequest struct
function getStringValue (line 502) | func getStringValue(s *string) string {
type CreateSceneRequest (line 509) | type CreateSceneRequest struct
FILE: application/services/storyboard_service.go
type StoryboardService (line 18) | type StoryboardService struct
method GenerateStoryboard (line 64) | func (s *StoryboardService) GenerateStoryboard(episodeID string, model...
method processStoryboardGeneration (line 348) | func (s *StoryboardService) processStoryboardGeneration(taskID, episod...
method generateImagePrompt (line 477) | func (s *StoryboardService) generateImagePrompt(sb Storyboard) string {
method generateVideoPrompt (line 621) | func (s *StoryboardService) generateVideoPrompt(sb Storyboard) string {
method saveStoryboards (line 685) | func (s *StoryboardService) saveStoryboards(episodeID string, storyboa...
method CreateStoryboard (line 895) | func (s *StoryboardService) CreateStoryboard(req *CreateStoryboardRequ...
method DeleteStoryboard (line 970) | func (s *StoryboardService) DeleteStoryboard(storyboardID uint) error {
function NewStoryboardService (line 27) | func NewStoryboardService(db *gorm.DB, cfg *config.Config, log *logger.L...
type Storyboard (line 38) | type Storyboard struct
type GenerateStoryboardResult (line 59) | type GenerateStoryboardResult struct
function extractInitialPose (line 512) | func extractInitialPose(action string) string {
function extractSimpleLocation (line 535) | func extractSimpleLocation(location string) string {
function extractSimplePose (line 559) | func extractSimplePose(action string) string {
function extractFirstFramePose (line 578) | func extractFirstFramePose(action string) string {
function extractCompositionType (line 600) | func extractCompositionType(shotType string) string {
type CreateStoryboardRequest (line 873) | type CreateStoryboardRequest struct
function min (line 981) | func min(a, b int) int {
function getString (line 988) | func getString(s *string) string {
FILE: application/services/storyboard_update_full.go
method UpdateStoryboard (line 10) | func (s *StoryboardService) UpdateStoryboard(storyboardID string, update...
FILE: application/services/task_service.go
type TaskService (line 14) | type TaskService struct
method CreateTask (line 27) | func (s *TaskService) CreateTask(taskType, resourceID string) (*models...
method UpdateTaskStatus (line 44) | func (s *TaskService) UpdateTaskStatus(taskID, status string, progress...
method UpdateTaskError (line 63) | func (s *TaskService) UpdateTaskError(taskID string, err error) error {
method UpdateTaskResult (line 77) | func (s *TaskService) UpdateTaskResult(taskID string, result interface...
method GetTask (line 96) | func (s *TaskService) GetTask(taskID string) (*models.AsyncTask, error) {
method GetTasksByResource (line 105) | func (s *TaskService) GetTasksByResource(resourceID string) ([]*models...
function NewTaskService (line 19) | func NewTaskService(db *gorm.DB, log *logger.Logger) *TaskService {
FILE: application/services/upload_service.go
type UploadService (line 15) | type UploadService struct
method UploadFile (line 41) | func (s *UploadService) UploadFile(file io.Reader, fileName, contentTy...
method UploadCharacterImage (line 81) | func (s *UploadService) UploadCharacterImage(file io.Reader, fileName,...
method DeleteFile (line 86) | func (s *UploadService) DeleteFile(fileURL string) error {
method extractRelativePathFromURL (line 106) | func (s *UploadService) extractRelativePathFromURL(fileURL string) str...
method GetPresignedURL (line 116) | func (s *UploadService) GetPresignedURL(objectName string, expiry time...
function NewUploadService (line 21) | func NewUploadService(cfg *config.Config, log *logger.Logger) (*UploadSe...
type UploadResult (line 35) | type UploadResult struct
FILE: application/services/video_generation_service.go
type VideoGenerationService (line 19) | type VideoGenerationService struct
method GenerateVideo (line 78) | func (s *VideoGenerationService) GenerateVideo(request *GenerateVideoR...
method ProcessVideoGeneration (line 190) | func (s *VideoGenerationService) ProcessVideoGeneration(videoGenID uin...
method pollTaskStatus (line 367) | func (s *VideoGenerationService) pollTaskStatus(videoGenID uint, taskI...
method completeVideoGeneration (line 449) | func (s *VideoGenerationService) completeVideoGeneration(videoGenID ui...
method updateVideoGenError (line 566) | func (s *VideoGenerationService) updateVideoGenError(videoGenID uint, ...
method getVideoClient (line 575) | func (s *VideoGenerationService) getVideoClient(provider string, model...
method RecoverPendingTasks (line 631) | func (s *VideoGenerationService) RecoverPendingTasks() {
method GetVideoGeneration (line 657) | func (s *VideoGenerationService) GetVideoGeneration(id uint) (*models....
method ListVideoGenerations (line 665) | func (s *VideoGenerationService) ListVideoGenerations(dramaID *uint, s...
method GenerateVideoFromImage (line 692) | func (s *VideoGenerationService) GenerateVideoFromImage(imageGenID uin...
method BatchGenerateVideosForEpisode (line 727) | func (s *VideoGenerationService) BatchGenerateVideosForEpisode(episode...
method DeleteVideoGeneration (line 758) | func (s *VideoGenerationService) DeleteVideoGeneration(id uint) error {
method convertImageToBase64 (line 764) | func (s *VideoGenerationService) convertImageToBase64(imageURL string)...
function NewVideoGenerationService (line 29) | func NewVideoGenerationService(db *gorm.DB, transferService *ResourceTra...
type GenerateVideoRequest (line 45) | type GenerateVideoRequest struct
FILE: application/services/video_merge_service.go
type VideoMergeService (line 19) | type VideoMergeService struct
method MergeVideos (line 50) | func (s *VideoMergeService) MergeVideos(req *MergeVideoRequest) (*mode...
method processMergeVideo (line 101) | func (s *VideoMergeService) processMergeVideo(mergeID uint) {
method mergeVideoClips (line 142) | func (s *VideoMergeService) mergeVideoClips(client video.VideoClient, ...
method pollMergeStatus (line 219) | func (s *VideoMergeService) pollMergeStatus(mergeID uint, client video...
method completeMerge (line 246) | func (s *VideoMergeService) completeMerge(mergeID uint, result *video....
method updateMergeError (line 285) | func (s *VideoMergeService) updateMergeError(mergeID uint, errorMsg st...
method getVideoClient (line 293) | func (s *VideoMergeService) getVideoClient(provider string) (video.Vid...
method GetMerge (line 333) | func (s *VideoMergeService) GetMerge(mergeID uint) (*models.VideoMerge...
method ListMerges (line 341) | func (s *VideoMergeService) ListMerges(episodeID *string, status strin...
method DeleteMerge (line 366) | func (s *VideoMergeService) DeleteMerge(mergeID uint) error {
method FinalizeEpisode (line 412) | func (s *VideoMergeService) FinalizeEpisode(episodeID string, timeline...
function NewVideoMergeService (line 29) | func NewVideoMergeService(db *gorm.DB, transferService *ResourceTransfer...
type MergeVideoRequest (line 41) | type MergeVideoRequest struct
type TimelineClip (line 378) | type TimelineClip struct
function getAssetIDString (line 389) | func getAssetIDString(assetID interface{}) string {
type FinalizeEpisodeRequest (line 406) | type FinalizeEpisodeRequest struct
FILE: cmd/migrate/main.go
type DataMigrationService (line 21) | type DataMigrationService struct
method MigrateLocalPaths (line 86) | func (s *DataMigrationService) MigrateLocalPaths() error {
method ensureStorageDirectories (line 150) | func (s *DataMigrationService) ensureStorageDirectories() error {
method migrateAssets (line 168) | func (s *DataMigrationService) migrateAssets(stats *MigrationStats) er...
method migrateCharacterLibraries (line 207) | func (s *DataMigrationService) migrateCharacterLibraries(stats *Migrat...
method migrateCharacters (line 241) | func (s *DataMigrationService) migrateCharacters(stats *MigrationStats...
method migrateImageGenerations (line 278) | func (s *DataMigrationService) migrateImageGenerations(stats *Migratio...
method migrateScenes (line 322) | func (s *DataMigrationService) migrateScenes(stats *MigrationStats) er...
method migrateVideoGenerations (line 359) | func (s *DataMigrationService) migrateVideoGenerations(stats *Migratio...
method downloadOrGetCached (line 396) | func (s *DataMigrationService) downloadOrGetCached(url, subDir, prefix...
method downloadAndSaveImage (line 426) | func (s *DataMigrationService) downloadAndSaveImage(imageURL, subDir, ...
method downloadAndSaveVideo (line 455) | func (s *DataMigrationService) downloadAndSaveVideo(videoURL, subDir, ...
method extractFileExtension (line 487) | func (s *DataMigrationService) extractFileExtension(url string) string {
method downloadFile (line 517) | func (s *DataMigrationService) downloadFile(url, filepath string) error {
type MigrationStats (line 29) | type MigrationStats struct
function main (line 44) | func main() {
FILE: domain/models/ai_config.go
type AIServiceConfig (line 10) | type AIServiceConfig struct
method TableName (line 28) | func (c *AIServiceConfig) TableName() string {
type AIServiceProvider (line 32) | type AIServiceProvider struct
method TableName (line 44) | func (p *AIServiceProvider) TableName() string {
type ModelField (line 49) | type ModelField
method Value (line 52) | func (m ModelField) Value() (driver.Value, error) {
method Scan (line 64) | func (m *ModelField) Scan(value interface{}) error {
method MarshalJSON (line 100) | func (m ModelField) MarshalJSON() ([]byte, error) {
method UnmarshalJSON (line 108) | func (m *ModelField) UnmarshalJSON(data []byte) error {
FILE: domain/models/asset.go
type Asset (line 9) | type Asset struct
method TableName (line 55) | func (Asset) TableName() string {
type AssetType (line 47) | type AssetType
constant AssetTypeImage (line 50) | AssetTypeImage AssetType = "image"
constant AssetTypeVideo (line 51) | AssetTypeVideo AssetType = "video"
constant AssetTypeAudio (line 52) | AssetTypeAudio AssetType = "audio"
FILE: domain/models/character_library.go
type CharacterLibrary (line 10) | type CharacterLibrary struct
method TableName (line 24) | func (c *CharacterLibrary) TableName() string {
FILE: domain/models/drama.go
type Drama (line 10) | type Drama struct
method TableName (line 32) | func (d *Drama) TableName() string {
type Character (line 36) | type Character struct
method TableName (line 62) | func (c *Character) TableName() string {
type Episode (line 66) | type Episode struct
method TableName (line 88) | func (e *Episode) TableName() string {
type Storyboard (line 92) | type Storyboard struct
method TableName (line 126) | func (s *Storyboard) TableName() string {
type Scene (line 130) | type Scene struct
method TableName (line 150) | func (s *Scene) TableName() string {
type Prop (line 154) | type Prop struct
method TableName (line 173) | func (p *Prop) TableName() string {
FILE: domain/models/frame_prompt.go
type FramePrompt (line 6) | type FramePrompt struct
method TableName (line 17) | func (FramePrompt) TableName() string {
constant FrameTypeFirst (line 23) | FrameTypeFirst = "first"
constant FrameTypeKey (line 24) | FrameTypeKey = "key"
constant FrameTypeLast (line 25) | FrameTypeLast = "last"
constant FrameTypePanel (line 26) | FrameTypePanel = "panel"
constant FrameTypeAction (line 27) | FrameTypeAction = "action"
FILE: domain/models/image_generation.go
type ImageGeneration (line 9) | type ImageGeneration struct
method TableName (line 48) | func (ImageGeneration) TableName() string {
type ImageGenerationStatus (line 52) | type ImageGenerationStatus
constant ImageStatusPending (line 55) | ImageStatusPending ImageGenerationStatus = "pending"
constant ImageStatusProcessing (line 56) | ImageStatusProcessing ImageGenerationStatus = "processing"
constant ImageStatusCompleted (line 57) | ImageStatusCompleted ImageGenerationStatus = "completed"
constant ImageStatusFailed (line 58) | ImageStatusFailed ImageGenerationStatus = "failed"
type ImageProvider (line 61) | type ImageProvider
constant ProviderOpenAI (line 64) | ProviderOpenAI ImageProvider = "openai"
constant ProviderMidjourney (line 65) | ProviderMidjourney ImageProvider = "midjourney"
constant ProviderStableDiffusion (line 66) | ProviderStableDiffusion ImageProvider = "stable_diffusion"
constant ProviderDALLE (line 67) | ProviderDALLE ImageProvider = "dalle"
type ImageType (line 71) | type ImageType
constant ImageTypeCharacter (line 74) | ImageTypeCharacter ImageType = "character"
constant ImageTypeScene (line 75) | ImageTypeScene ImageType = "scene"
constant ImageTypeProp (line 76) | ImageTypeProp ImageType = "prop"
constant ImageTypeStoryboard (line 77) | ImageTypeStoryboard ImageType = "storyboard"
FILE: domain/models/task.go
type AsyncTask (line 10) | type AsyncTask struct
FILE: domain/models/timeline.go
type Timeline (line 9) | type Timeline struct
method TableName (line 42) | func (Timeline) TableName() string {
type TimelineStatus (line 33) | type TimelineStatus
constant TimelineStatusDraft (line 36) | TimelineStatusDraft TimelineStatus = "draft"
constant TimelineStatusEditing (line 37) | TimelineStatusEditing TimelineStatus = "editing"
constant TimelineStatusCompleted (line 38) | TimelineStatusCompleted TimelineStatus = "completed"
constant TimelineStatusExporting (line 39) | TimelineStatusExporting TimelineStatus = "exporting"
type TimelineTrack (line 46) | type TimelineTrack struct
method TableName (line 73) | func (TimelineTrack) TableName() string {
type TrackType (line 65) | type TrackType
constant TrackTypeVideo (line 68) | TrackTypeVideo TrackType = "video"
constant TrackTypeAudio (line 69) | TrackTypeAudio TrackType = "audio"
constant TrackTypeText (line 70) | TrackTypeText TrackType = "text"
type TimelineClip (line 77) | type TimelineClip struct
method TableName (line 116) | func (TimelineClip) TableName() string {
type ClipTransition (line 120) | type ClipTransition struct
method TableName (line 144) | func (ClipTransition) TableName() string {
type TransitionType (line 133) | type TransitionType
constant TransitionTypeFade (line 136) | TransitionTypeFade TransitionType = "fade"
constant TransitionTypeCrossFade (line 137) | TransitionTypeCrossFade TransitionType = "crossfade"
constant TransitionTypeSlide (line 138) | TransitionTypeSlide TransitionType = "slide"
constant TransitionTypeWipe (line 139) | TransitionTypeWipe TransitionType = "wipe"
constant TransitionTypeZoom (line 140) | TransitionTypeZoom TransitionType = "zoom"
constant TransitionTypeDissolve (line 141) | TransitionTypeDissolve TransitionType = "dissolve"
type ClipEffect (line 148) | type ClipEffect struct
method TableName (line 176) | func (ClipEffect) TableName() string {
type EffectType (line 165) | type EffectType
constant EffectTypeFilter (line 168) | EffectTypeFilter EffectType = "filter"
constant EffectTypeColor (line 169) | EffectTypeColor EffectType = "color"
constant EffectTypeBlur (line 170) | EffectTypeBlur EffectType = "blur"
constant EffectTypeBrightness (line 171) | EffectTypeBrightness EffectType = "brightness"
constant EffectTypeContrast (line 172) | EffectTypeContrast EffectType = "contrast"
constant EffectTypeSaturation (line 173) | EffectTypeSaturation EffectType = "saturation"
FILE: domain/models/video_generation.go
type VideoGeneration (line 9) | type VideoGeneration struct
method TableName (line 77) | func (VideoGeneration) TableName() string {
type VideoStatus (line 59) | type VideoStatus
constant VideoStatusPending (line 62) | VideoStatusPending VideoStatus = "pending"
constant VideoStatusProcessing (line 63) | VideoStatusProcessing VideoStatus = "processing"
constant VideoStatusCompleted (line 64) | VideoStatusCompleted VideoStatus = "completed"
constant VideoStatusFailed (line 65) | VideoStatusFailed VideoStatus = "failed"
type VideoProvider (line 68) | type VideoProvider
constant VideoProviderRunway (line 71) | VideoProviderRunway VideoProvider = "runway"
constant VideoProviderPika (line 72) | VideoProviderPika VideoProvider = "pika"
constant VideoProviderDoubao (line 73) | VideoProviderDoubao VideoProvider = "doubao"
constant VideoProviderOpenAI (line 74) | VideoProviderOpenAI VideoProvider = "openai"
FILE: domain/models/video_merge.go
type VideoMergeStatus (line 10) | type VideoMergeStatus
constant VideoMergeStatusPending (line 13) | VideoMergeStatusPending VideoMergeStatus = "pending"
constant VideoMergeStatusProcessing (line 14) | VideoMergeStatusProcessing VideoMergeStatus = "processing"
constant VideoMergeStatusCompleted (line 15) | VideoMergeStatusCompleted VideoMergeStatus = "completed"
constant VideoMergeStatusFailed (line 16) | VideoMergeStatusFailed VideoMergeStatus = "failed"
type VideoMerge (line 19) | type VideoMerge struct
method TableName (line 50) | func (v *VideoMerge) TableName() string {
type SceneClip (line 40) | type SceneClip struct
FILE: infrastructure/database/custom_logger.go
type CustomLogger (line 12) | type CustomLogger struct
method Trace (line 24) | func (l *CustomLogger) Trace(ctx context.Context, begin time.Time, fc ...
method Info (line 71) | func (l *CustomLogger) Info(ctx context.Context, msg string, data ...i...
method Warn (line 76) | func (l *CustomLogger) Warn(ctx context.Context, msg string, data ...i...
method Error (line 81) | func (l *CustomLogger) Error(ctx context.Context, msg string, data ......
method LogMode (line 99) | func (l *CustomLogger) LogMode(level logger.LogLevel) logger.Interface {
function NewCustomLogger (line 17) | func NewCustomLogger() logger.Interface {
function truncateLongValues (line 40) | func truncateLongValues(sql string) string {
FILE: infrastructure/database/database.go
function NewDatabase (line 17) | func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
function AutoMigrate (line 72) | func AutoMigrate(db *gorm.DB) error {
FILE: infrastructure/external/ffmpeg/ffmpeg.go
type FFmpeg (line 16) | type FFmpeg struct
method MergeVideos (line 44) | func (f *FFmpeg) MergeVideos(opts *MergeOptions) (string, error) {
method downloadVideo (line 107) | func (f *FFmpeg) downloadVideo(url, destPath string) (string, error) {
method trimVideo (line 164) | func (f *FFmpeg) trimVideo(inputPath, outputPath string, startTime, en...
method concatenateVideosWithTransitions (line 244) | func (f *FFmpeg) concatenateVideosWithTransitions(inputPaths []string,...
method concatenateVideos (line 275) | func (f *FFmpeg) concatenateVideos(inputPaths []string, outputPath str...
method mergeWithXfade (line 313) | func (f *FFmpeg) mergeWithXfade(inputPaths []string, clips []VideoClip...
method mapTransitionType (line 630) | func (f *FFmpeg) mapTransitionType(transType string) string {
method hasAudioStream (line 693) | func (f *FFmpeg) hasAudioStream(videoPath string) bool {
method getVideoResolution (line 711) | func (f *FFmpeg) getVideoResolution(videoPath string) (int, int) {
method GetVideoDuration (line 745) | func (f *FFmpeg) GetVideoDuration(videoPath string) (float64, error) {
method copyFile (line 774) | func (f *FFmpeg) copyFile(src, dst string) error {
method cleanup (line 784) | func (f *FFmpeg) cleanup(paths []string) {
method CleanupTempDir (line 792) | func (f *FFmpeg) CleanupTempDir() error {
method ExtractAudio (line 798) | func (f *FFmpeg) ExtractAudio(videoURL, outputPath string) (string, er...
method generateSilence (line 855) | func (f *FFmpeg) generateSilence(outputPath string, duration float64) ...
function NewFFmpeg (line 21) | func NewFFmpeg(log *logger.Logger) *FFmpeg {
type VideoClip (line 31) | type VideoClip struct
type MergeOptions (line 39) | type MergeOptions struct
FILE: infrastructure/scheduler/resource_transfer_scheduler.go
type ResourceTransferScheduler (line 12) | type ResourceTransferScheduler struct
method Start (line 35) | func (s *ResourceTransferScheduler) Start() error {
method Stop (line 69) | func (s *ResourceTransferScheduler) Stop() {
method transferPendingResources (line 82) | func (s *ResourceTransferScheduler) transferPendingResources() {
method transferAllPendingResources (line 155) | func (s *ResourceTransferScheduler) transferAllPendingResources() {
method RunNow (line 231) | func (s *ResourceTransferScheduler) RunNow() {
method RunFullScan (line 237) | func (s *ResourceTransferScheduler) RunFullScan() {
function NewResourceTransferScheduler (line 20) | func NewResourceTransferScheduler(
FILE: infrastructure/storage/local_storage.go
type LocalStorage (line 15) | type LocalStorage struct
method Upload (line 31) | func (s *LocalStorage) Upload(file io.Reader, filename string, categor...
method Delete (line 55) | func (s *LocalStorage) Delete(url string) error {
method GetURL (line 59) | func (s *LocalStorage) GetURL(path string) string {
method DownloadFromURL (line 71) | func (s *LocalStorage) DownloadFromURL(url, category string) (string, ...
method DownloadFromURLWithPath (line 80) | func (s *LocalStorage) DownloadFromURLWithPath(url, category string) (...
method GetAbsolutePath (line 136) | func (s *LocalStorage) GetAbsolutePath(relativePath string) string {
function NewLocalStorage (line 20) | func NewLocalStorage(basePath, baseURL string) (*LocalStorage, error) {
type DownloadResult (line 64) | type DownloadResult struct
function getFileExtension (line 141) | func getFileExtension(url, contentType string) string {
FILE: main.go
function main (line 21) | func main() {
FILE: migrations/init.sql
type dramas (line 10) | CREATE TABLE IF NOT EXISTS dramas (
type idx_dramas_status (line 27) | CREATE INDEX IF NOT EXISTS idx_dramas_status ON dramas(status)
type idx_dramas_deleted_at (line 28) | CREATE INDEX IF NOT EXISTS idx_dramas_deleted_at ON dramas(deleted_at)
type episodes (line 31) | CREATE TABLE IF NOT EXISTS episodes (
type idx_episodes_drama_id (line 47) | CREATE INDEX IF NOT EXISTS idx_episodes_drama_id ON episodes(drama_id)
type idx_episodes_status (line 48) | CREATE INDEX IF NOT EXISTS idx_episodes_status ON episodes(status)
type idx_episodes_deleted_at (line 49) | CREATE INDEX IF NOT EXISTS idx_episodes_deleted_at ON episodes(deleted_at)
type characters (line 52) | CREATE TABLE IF NOT EXISTS characters (
type idx_characters_drama_id (line 71) | CREATE INDEX IF NOT EXISTS idx_characters_drama_id ON characters(drama_id)
type idx_characters_deleted_at (line 72) | CREATE INDEX IF NOT EXISTS idx_characters_deleted_at ON characters(delet...
type scenes (line 75) | CREATE TABLE IF NOT EXISTS scenes (
type idx_scenes_drama_id (line 90) | CREATE INDEX IF NOT EXISTS idx_scenes_drama_id ON scenes(drama_id)
type idx_scenes_status (line 91) | CREATE INDEX IF NOT EXISTS idx_scenes_status ON scenes(status)
type idx_scenes_deleted_at (line 92) | CREATE INDEX IF NOT EXISTS idx_scenes_deleted_at ON scenes(deleted_at)
type props (line 95) | CREATE TABLE IF NOT EXISTS props (
type idx_props_drama_id (line 110) | CREATE INDEX IF NOT EXISTS idx_props_drama_id ON props(drama_id)
type idx_props_deleted_at (line 111) | CREATE INDEX IF NOT EXISTS idx_props_deleted_at ON props(deleted_at)
type storyboards (line 114) | CREATE TABLE IF NOT EXISTS storyboards (
type idx_storyboards_episode_id (line 138) | CREATE INDEX IF NOT EXISTS idx_storyboards_episode_id ON storyboards(epi...
type idx_storyboards_scene_id (line 139) | CREATE INDEX IF NOT EXISTS idx_storyboards_scene_id ON storyboards(scene...
type idx_storyboards_storyboard_number (line 140) | CREATE INDEX IF NOT EXISTS idx_storyboards_storyboard_number ON storyboa...
type idx_storyboards_status (line 141) | CREATE INDEX IF NOT EXISTS idx_storyboards_status ON storyboards(status)
type idx_storyboards_deleted_at (line 142) | CREATE INDEX IF NOT EXISTS idx_storyboards_deleted_at ON storyboards(del...
type image_generations (line 149) | CREATE TABLE IF NOT EXISTS image_generations (
type idx_image_generations_storyboard_id (line 178) | CREATE INDEX IF NOT EXISTS idx_image_generations_storyboard_id ON image_...
type idx_image_generations_drama_id (line 179) | CREATE INDEX IF NOT EXISTS idx_image_generations_drama_id ON image_gener...
type idx_image_generations_status (line 180) | CREATE INDEX IF NOT EXISTS idx_image_generations_status ON image_generat...
type idx_image_generations_task_id (line 181) | CREATE INDEX IF NOT EXISTS idx_image_generations_task_id ON image_genera...
type idx_image_generations_deleted_at (line 182) | CREATE INDEX IF NOT EXISTS idx_image_generations_deleted_at ON image_gen...
type video_generations (line 185) | CREATE TABLE IF NOT EXISTS video_generations (
type idx_video_generations_storyboard_id (line 217) | CREATE INDEX IF NOT EXISTS idx_video_generations_storyboard_id ON video_...
type idx_video_generations_drama_id (line 218) | CREATE INDEX IF NOT EXISTS idx_video_generations_drama_id ON video_gener...
type idx_video_generations_provider (line 219) | CREATE INDEX IF NOT EXISTS idx_video_generations_provider ON video_gener...
type idx_video_generations_status (line 220) | CREATE INDEX IF NOT EXISTS idx_video_generations_status ON video_generat...
type idx_video_generations_task_id (line 221) | CREATE INDEX IF NOT EXISTS idx_video_generations_task_id ON video_genera...
type idx_video_generations_image_gen_id (line 222) | CREATE INDEX IF NOT EXISTS idx_video_generations_image_gen_id ON video_g...
type idx_video_generations_deleted_at (line 223) | CREATE INDEX IF NOT EXISTS idx_video_generations_deleted_at ON video_gen...
type video_merges (line 226) | CREATE TABLE IF NOT EXISTS video_merges (
type idx_video_merges_episode_id (line 244) | CREATE INDEX IF NOT EXISTS idx_video_merges_episode_id ON video_merges(e...
type idx_video_merges_drama_id (line 245) | CREATE INDEX IF NOT EXISTS idx_video_merges_drama_id ON video_merges(dra...
type idx_video_merges_status (line 246) | CREATE INDEX IF NOT EXISTS idx_video_merges_status ON video_merges(status)
type idx_video_merges_deleted_at (line 247) | CREATE INDEX IF NOT EXISTS idx_video_merges_deleted_at ON video_merges(d...
type character_libraries (line 254) | CREATE TABLE IF NOT EXISTS character_libraries (
type idx_character_libraries_category (line 268) | CREATE INDEX IF NOT EXISTS idx_character_libraries_category ON character...
type idx_character_libraries_deleted_at (line 269) | CREATE INDEX IF NOT EXISTS idx_character_libraries_deleted_at ON charact...
type timelines (line 276) | CREATE TABLE IF NOT EXISTS timelines (
type idx_timelines_drama_id (line 291) | CREATE INDEX IF NOT EXISTS idx_timelines_drama_id ON timelines(drama_id)
type idx_timelines_episode_id (line 292) | CREATE INDEX IF NOT EXISTS idx_timelines_episode_id ON timelines(episode...
type idx_timelines_status (line 293) | CREATE INDEX IF NOT EXISTS idx_timelines_status ON timelines(status)
type idx_timelines_deleted_at (line 294) | CREATE INDEX IF NOT EXISTS idx_timelines_deleted_at ON timelines(deleted...
type timeline_tracks (line 297) | CREATE TABLE IF NOT EXISTS timeline_tracks (
type idx_timeline_tracks_timeline_id (line 311) | CREATE INDEX IF NOT EXISTS idx_timeline_tracks_timeline_id ON timeline_t...
type idx_timeline_tracks_type (line 312) | CREATE INDEX IF NOT EXISTS idx_timeline_tracks_type ON timeline_tracks(t...
type idx_timeline_tracks_deleted_at (line 313) | CREATE INDEX IF NOT EXISTS idx_timeline_tracks_deleted_at ON timeline_tr...
type timeline_clips (line 316) | CREATE TABLE IF NOT EXISTS timeline_clips (
type idx_timeline_clips_track_id (line 339) | CREATE INDEX IF NOT EXISTS idx_timeline_clips_track_id ON timeline_clips...
type idx_timeline_clips_asset_id (line 340) | CREATE INDEX IF NOT EXISTS idx_timeline_clips_asset_id ON timeline_clips...
type idx_timeline_clips_storyboard_id (line 341) | CREATE INDEX IF NOT EXISTS idx_timeline_clips_storyboard_id ON timeline_...
type idx_timeline_clips_transition_in (line 342) | CREATE INDEX IF NOT EXISTS idx_timeline_clips_transition_in ON timeline_...
type idx_timeline_clips_transition_out (line 343) | CREATE INDEX IF NOT EXISTS idx_timeline_clips_transition_out ON timeline...
type idx_timeline_clips_deleted_at (line 344) | CREATE INDEX IF NOT EXISTS idx_timeline_clips_deleted_at ON timeline_cli...
type clip_transitions (line 347) | CREATE TABLE IF NOT EXISTS clip_transitions (
type idx_clip_transitions_type (line 358) | CREATE INDEX IF NOT EXISTS idx_clip_transitions_type ON clip_transitions...
type idx_clip_transitions_deleted_at (line 359) | CREATE INDEX IF NOT EXISTS idx_clip_transitions_deleted_at ON clip_trans...
type clip_effects (line 362) | CREATE TABLE IF NOT EXISTS clip_effects (
type idx_clip_effects_clip_id (line 375) | CREATE INDEX IF NOT EXISTS idx_clip_effects_clip_id ON clip_effects(clip...
type idx_clip_effects_type (line 376) | CREATE INDEX IF NOT EXISTS idx_clip_effects_type ON clip_effects(type)
type idx_clip_effects_deleted_at (line 377) | CREATE INDEX IF NOT EXISTS idx_clip_effects_deleted_at ON clip_effects(d...
type assets (line 384) | CREATE TABLE IF NOT EXISTS assets (
type idx_assets_drama_id (line 409) | CREATE INDEX IF NOT EXISTS idx_assets_drama_id ON assets(drama_id)
type idx_assets_type (line 410) | CREATE INDEX IF NOT EXISTS idx_assets_type ON assets(type)
type idx_assets_category (line 411) | CREATE INDEX IF NOT EXISTS idx_assets_category ON assets(category)
type idx_assets_image_gen_id (line 412) | CREATE INDEX IF NOT EXISTS idx_assets_image_gen_id ON assets(image_gen_id)
type idx_assets_video_gen_id (line 413) | CREATE INDEX IF NOT EXISTS idx_assets_video_gen_id ON assets(video_gen_id)
type idx_assets_deleted_at (line 414) | CREATE INDEX IF NOT EXISTS idx_assets_deleted_at ON assets(deleted_at)
type asset_tags (line 417) | CREATE TABLE IF NOT EXISTS asset_tags (
type idx_asset_tags_deleted_at (line 426) | CREATE INDEX IF NOT EXISTS idx_asset_tags_deleted_at ON asset_tags(delet...
type asset_collections (line 429) | CREATE TABLE IF NOT EXISTS asset_collections (
type idx_asset_collections_drama_id (line 439) | CREATE INDEX IF NOT EXISTS idx_asset_collections_drama_id ON asset_colle...
type idx_asset_collections_deleted_at (line 440) | CREATE INDEX IF NOT EXISTS idx_asset_collections_deleted_at ON asset_col...
type asset_tag_relations (line 443) | CREATE TABLE IF NOT EXISTS asset_tag_relations (
type idx_asset_tag_relations_asset_id (line 449) | CREATE INDEX IF NOT EXISTS idx_asset_tag_relations_asset_id ON asset_tag...
type idx_asset_tag_relations_tag_id (line 450) | CREATE INDEX IF NOT EXISTS idx_asset_tag_relations_tag_id ON asset_tag_r...
type asset_collection_relations (line 453) | CREATE TABLE IF NOT EXISTS asset_collection_relations (
type idx_asset_collection_relations_asset_id (line 459) | CREATE INDEX IF NOT EXISTS idx_asset_collection_relations_asset_id ON as...
type idx_asset_collection_relations_collection_id (line 460) | CREATE INDEX IF NOT EXISTS idx_asset_collection_relations_collection_id ...
type ai_service_configs (line 467) | CREATE TABLE IF NOT EXISTS ai_service_configs (
type idx_ai_service_configs_service_type (line 486) | CREATE INDEX IF NOT EXISTS idx_ai_service_configs_service_type ON ai_ser...
type idx_ai_service_configs_deleted_at (line 487) | CREATE INDEX IF NOT EXISTS idx_ai_service_configs_deleted_at ON ai_servi...
type ai_service_providers (line 490) | CREATE TABLE IF NOT EXISTS ai_service_providers (
type idx_ai_service_providers_service_type (line 503) | CREATE INDEX IF NOT EXISTS idx_ai_service_providers_service_type ON ai_s...
type idx_ai_service_providers_deleted_at (line 504) | CREATE INDEX IF NOT EXISTS idx_ai_service_providers_deleted_at ON ai_ser...
FILE: pkg/ai/client.go
type AIClient (line 4) | type AIClient interface
FILE: pkg/ai/gemini_client.go
type GeminiClient (line 13) | type GeminiClient struct
method GenerateText (line 82) | func (c *GeminiClient) GenerateText(prompt string, systemPrompt string...
method GenerateImage (line 186) | func (c *GeminiClient) GenerateImage(prompt string, size string, n int...
method TestConnection (line 190) | func (c *GeminiClient) TestConnection() error {
type GeminiTextRequest (line 21) | type GeminiTextRequest struct
type GeminiContent (line 26) | type GeminiContent struct
type GeminiPart (line 31) | type GeminiPart struct
type GeminiInstruction (line 35) | type GeminiInstruction struct
type GeminiTextResponse (line 39) | type GeminiTextResponse struct
function NewGeminiClient (line 61) | func NewGeminiClient(baseURL, apiKey, model, endpoint string) *GeminiCli...
FILE: pkg/ai/openai_client.go
type OpenAIClient (line 13) | type OpenAIClient struct
method ChatCompletion (line 95) | func (c *OpenAIClient) ChatCompletion(messages []ChatMessage, options ...
method sendChatRequest (line 108) | func (c *OpenAIClient) sendChatRequest(req *ChatCompletionRequest) (*C...
method doChatRequest (line 126) | func (c *OpenAIClient) doChatRequest(req *ChatCompletionRequest) (*Cha...
method GenerateText (line 240) | func (c *OpenAIClient) GenerateText(prompt string, systemPrompt string...
method GenerateImage (line 267) | func (c *OpenAIClient) GenerateImage(prompt string, size string, n int...
method TestConnection (line 333) | func (c *OpenAIClient) TestConnection() error {
type ChatMessage (line 21) | type ChatMessage struct
type ChatCompletionRequest (line 26) | type ChatCompletionRequest struct
type ChatCompletionResponse (line 36) | type ChatCompletionResponse struct
type ImageGenerationRequest (line 56) | type ImageGenerationRequest struct
type ImageGenerationResponse (line 63) | type ImageGenerationResponse struct
type ErrorResponse (line 71) | type ErrorResponse struct
function NewOpenAIClient (line 79) | func NewOpenAIClient(baseURL, apiKey, model, endpoint string) *OpenAICli...
function WithTemperature (line 222) | func WithTemperature(temp float64) func(*ChatCompletionRequest) {
function WithMaxTokens (line 228) | func WithMaxTokens(tokens int) func(*ChatCompletionRequest) {
function WithTopP (line 234) | func WithTopP(topP float64) func(*ChatCompletionRequest) {
function shouldRetryWithMaxCompletionTokens (line 352) | func shouldRetryWithMaxCompletionTokens(err error, req *ChatCompletionRe...
FILE: pkg/config/config.go
type Config (line 9) | type Config struct
type AppConfig (line 17) | type AppConfig struct
type ServerConfig (line 24) | type ServerConfig struct
type DatabaseConfig (line 32) | type DatabaseConfig struct
method DSN (line 77) | func (c *DatabaseConfig) DSN() string {
type StorageConfig (line 45) | type StorageConfig struct
type AIConfig (line 51) | type AIConfig struct
function LoadConfig (line 57) | func LoadConfig() (*Config, error) {
FILE: pkg/image/gemini_image_client.go
type GeminiImageClient (line 14) | type GeminiImageClient struct
method GenerateImage (line 108) | func (c *GeminiImageClient) GenerateImage(prompt string, opts ...Image...
method GetTaskStatus (line 267) | func (c *GeminiImageClient) GetTaskStatus(taskID string) (*ImageResult...
type GeminiImageRequest (line 22) | type GeminiImageRequest struct
type GeminiPart (line 31) | type GeminiPart struct
type GeminiInlineData (line 36) | type GeminiInlineData struct
type GeminiImageResponse (line 41) | type GeminiImageResponse struct
function downloadImageToBase64 (line 61) | func downloadImageToBase64(imageURL string) (string, string, error) {
function NewGeminiImageClient (line 87) | func NewGeminiImageClient(baseURL, apiKey, model, endpoint string) *Gemi...
function replaceModelPlaceholder (line 271) | func replaceModelPlaceholder(endpoint, model string) string {
FILE: pkg/image/image_client.go
type ImageClient (line 3) | type ImageClient interface
type ImageResult (line 8) | type ImageResult struct
type ImageOptions (line 18) | type ImageOptions struct
type ImageOption (line 32) | type ImageOption
function WithNegativePrompt (line 34) | func WithNegativePrompt(prompt string) ImageOption {
function WithSize (line 40) | func WithSize(size string) ImageOption {
function WithQuality (line 46) | func WithQuality(quality string) ImageOption {
function WithStyle (line 52) | func WithStyle(style string) ImageOption {
function WithSteps (line 58) | func WithSteps(steps int) ImageOption {
function WithCfgScale (line 64) | func WithCfgScale(scale float64) ImageOption {
function WithSeed (line 70) | func WithSeed(seed int64) ImageOption {
function WithModel (line 76) | func WithModel(model string) ImageOption {
function WithDimensions (line 82) | func WithDimensions(width, height int) ImageOption {
function WithReferenceImages (line 89) | func WithReferenceImages(images []string) ImageOption {
FILE: pkg/image/openai_image_client.go
type OpenAIImageClient (line 12) | type OpenAIImageClient struct
method GenerateImage (line 52) | func (c *OpenAIImageClient) GenerateImage(prompt string, opts ...Image...
method GetTaskStatus (line 126) | func (c *OpenAIImageClient) GetTaskStatus(taskID string) (*ImageResult...
type DALLERequest (line 20) | type DALLERequest struct
type DALLEResponse (line 29) | type DALLEResponse struct
function NewOpenAIImageClient (line 37) | func NewOpenAIImageClient(baseURL, apiKey, model, endpoint string) *Open...
FILE: pkg/image/volcengine_image_client.go
type VolcEngineImageClient (line 12) | type VolcEngineImageClient struct
method GenerateImage (line 64) | func (c *VolcEngineImageClient) GenerateImage(prompt string, opts ...I...
method GetTaskStatus (line 156) | func (c *VolcEngineImageClient) GetTaskStatus(taskID string) (*ImageRe...
type VolcEngineImageRequest (line 21) | type VolcEngineImageRequest struct
type VolcEngineImageResponse (line 30) | type VolcEngineImageResponse struct
function NewVolcEngineImageClient (line 45) | func NewVolcEngineImageClient(baseURL, apiKey, model, endpoint, queryEnd...
FILE: pkg/logger/logger.go
type Logger (line 8) | type Logger struct
function NewLogger (line 12) | func NewLogger(debug bool) *Logger {
FILE: pkg/response/response.go
type Response (line 10) | type Response struct
type ErrorInfo (line 18) | type ErrorInfo struct
type PaginationData (line 24) | type PaginationData struct
type Pagination (line 29) | type Pagination struct
function Success (line 36) | func Success(c *gin.Context, data interface{}) {
function SuccessWithMessage (line 44) | func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
function Created (line 53) | func Created(c *gin.Context, data interface{}) {
function SuccessWithPagination (line 61) | func SuccessWithPagination(c *gin.Context, items interface{}, total int6...
function Error (line 78) | func Error(c *gin.Context, statusCode int, errCode string, message strin...
function ErrorWithDetails (line 89) | func ErrorWithDetails(c *gin.Context, statusCode int, errCode string, me...
function BadRequest (line 101) | func BadRequest(c *gin.Context, message string) {
function Unauthorized (line 105) | func Unauthorized(c *gin.Context, message string) {
function Forbidden (line 109) | func Forbidden(c *gin.Context, message string) {
function NotFound (line 113) | func NotFound(c *gin.Context, message string) {
function InternalError (line 117) | func InternalError(c *gin.Context, message string) {
FILE: pkg/utils/image_utils.go
function ImageToBase64 (line 14) | func ImageToBase64(imagePath string) (string, error) {
function downloadImageFromURL (line 43) | func downloadImageFromURL(url string) ([]byte, error) {
function detectImageMimeType (line 58) | func detectImageMimeType(data []byte) string {
FILE: pkg/utils/json_parser.go
function SafeParseAIJSON (line 17) | func SafeParseAIJSON(aiResponse string, v interface{}) error {
function attemptJSONRepair (line 109) | func attemptJSONRepair(jsonStr string) string {
function ExtractJSONFromText (line 170) | func ExtractJSONFromText(text string) string {
function ValidateJSON (line 196) | func ValidateJSON(jsonStr string) error {
function isTruncated (line 202) | func isTruncated(jsonStr string) bool {
function truncateString (line 234) | func truncateString(s string, maxLen int) string {
function maxInt (line 241) | func maxInt(a, b int) int {
function minInt (line 248) | func minInt(a, b int) int {
FILE: pkg/utils/json_parser_test.go
function TestAttemptJSONRepairExcessBraces (line 10) | func TestAttemptJSONRepairExcessBraces(t *testing.T) {
function TestAttemptJSONRepairFunction (line 81) | func TestAttemptJSONRepairFunction(t *testing.T) {
FILE: pkg/video/chatfire_client.go
type ChatfireClient (line 14) | type ChatfireClient struct
method GenerateVideo (line 125) | func (c *ChatfireClient) GenerateVideo(imageURL, prompt string, opts ....
method GetTaskStatus (line 339) | func (c *ChatfireClient) GetTaskStatus(taskID string) (*VideoResult, e...
type ChatfireRequest (line 23) | type ChatfireRequest struct
type ChatfireSoraRequest (line 32) | type ChatfireSoraRequest struct
type ChatfireDoubaoRequest (line 41) | type ChatfireDoubaoRequest struct
type ChatfireResponse (line 51) | type ChatfireResponse struct
type ChatfireTaskResponse (line 63) | type ChatfireTaskResponse struct
function getErrorMessage (line 80) | func getErrorMessage(errorData json.RawMessage) string {
function NewChatfireClient (line 106) | func NewChatfireClient(baseURL, apiKey, model, endpoint, queryEndpoint s...
FILE: pkg/video/minimax_client.go
constant ModelHailuo23 (line 17) | ModelHailuo23 = "MiniMax-Hailuo-2.3"
constant ModelHailuo23Fast (line 22) | ModelHailuo23Fast = "MiniMax-Hailuo-2.3-Fast"
constant ModelHailuo02 (line 27) | ModelHailuo02 = "MiniMax-Hailuo-02"
constant Resolution768P (line 32) | Resolution768P = "768P"
constant Resolution1080P (line 33) | Resolution1080P = "1080P"
constant Duration6s (line 38) | Duration6s = 6
constant Duration10s (line 39) | Duration10s = 10
type MinimaxClient (line 43) | type MinimaxClient struct
method GenerateVideo (line 116) | func (c *MinimaxClient) GenerateVideo(imageURL, prompt string, opts .....
method GetTaskStatus (line 206) | func (c *MinimaxClient) GetTaskStatus(taskID string) (*VideoResult, er...
method getFileDownloadURL (line 266) | func (c *MinimaxClient) getFileDownloadURL(fileID string) (string, err...
type MinimaxSubjectReference (line 50) | type MinimaxSubjectReference struct
type MinimaxRequest (line 55) | type MinimaxRequest struct
type MinimaxCreateResponse (line 66) | type MinimaxCreateResponse struct
type MinimaxQueryResponse (line 75) | type MinimaxQueryResponse struct
type MinimaxFileResponse (line 88) | type MinimaxFileResponse struct
function NewMinimaxClient (line 103) | func NewMinimaxClient(baseURL, apiKey, model string) *MinimaxClient {
FILE: pkg/video/openai_sora_client.go
type OpenAISoraClient (line 17) | type OpenAISoraClient struct
method GenerateVideo (line 56) | func (c *OpenAISoraClient) GenerateVideo(imageURL, prompt string, opts...
method GetTaskStatus (line 234) | func (c *OpenAISoraClient) GetTaskStatus(taskID string) (*VideoResult,...
type OpenAISoraResponse (line 24) | type OpenAISoraResponse struct
function NewOpenAISoraClient (line 45) | func NewOpenAISoraClient(baseURL, apiKey, model string) *OpenAISoraClient {
FILE: pkg/video/video_client.go
type VideoClient (line 12) | type VideoClient interface
type VideoResult (line 17) | type VideoResult struct
type VideoOptions (line 29) | type VideoOptions struct
type VideoOption (line 44) | type VideoOption
function WithModel (line 46) | func WithModel(model string) VideoOption {
function WithDuration (line 52) | func WithDuration(duration int) VideoOption {
function WithFPS (line 58) | func WithFPS(fps int) VideoOption {
function WithResolution (line 64) | func WithResolution(resolution string) VideoOption {
function WithAspectRatio (line 70) | func WithAspectRatio(ratio string) VideoOption {
function WithStyle (line 76) | func WithStyle(style string) VideoOption {
function WithMotionLevel (line 82) | func WithMotionLevel(level int) VideoOption {
function WithCameraMotion (line 88) | func WithCameraMotion(motion string) VideoOption {
function WithSeed (line 94) | func WithSeed(seed int64) VideoOption {
function WithFirstFrame (line 100) | func WithFirstFrame(url string) VideoOption {
function WithLastFrame (line 106) | func WithLastFrame(url string) VideoOption {
function WithReferenceImages (line 112) | func WithReferenceImages(urls []string) VideoOption {
type RunwayClient (line 118) | type RunwayClient struct
method GenerateVideo (line 154) | func (c *RunwayClient) GenerateVideo(imageURL, prompt string, opts ......
method GetTaskStatus (line 229) | func (c *RunwayClient) GetTaskStatus(taskID string) (*VideoResult, err...
type RunwayRequest (line 125) | type RunwayRequest struct
type RunwayResponse (line 134) | type RunwayResponse struct
function NewRunwayClient (line 143) | func NewRunwayClient(baseURL, apiKey, model string) *RunwayClient {
type PikaClient (line 271) | type PikaClient struct
method GenerateVideo (line 309) | func (c *PikaClient) GenerateVideo(imageURL, prompt string, opts ...Vi...
method GetTaskStatus (line 387) | func (c *PikaClient) GetTaskStatus(taskID string) (*VideoResult, error) {
type PikaRequest (line 278) | type PikaRequest struct
type PikaResponse (line 289) | type PikaResponse struct
function NewPikaClient (line 298) | func NewPikaClient(baseURL, apiKey, model string) *PikaClient {
FILE: pkg/video/volces_ark_client.go
type VolcesArkClient (line 14) | type VolcesArkClient struct
method GenerateVideo (line 80) | func (c *VolcesArkClient) GenerateVideo(imageURL, prompt string, opts ...
method GetTaskStatus (line 232) | func (c *VolcesArkClient) GetTaskStatus(taskID string) (*VideoResult, ...
type VolcesArkContent (line 23) | type VolcesArkContent struct
type VolcesArkRequest (line 30) | type VolcesArkRequest struct
type VolcesArkResponse (line 36) | type VolcesArkResponse struct
function NewVolcesArkClient (line 60) | func NewVolcesArkClient(baseURL, apiKey, model, endpoint, queryEndpoint ...
FILE: web/public/ffmpeg/ffmpeg-core.js
function stringToPtr (line 8) | function stringToPtr(str){const len=Module["lengthBytesUTF8"](str)+1;con...
function stringsToPtr (line 8) | function stringsToPtr(strs){const len=strs.length;const ptr=Module["_mal...
function print (line 8) | function print(message){Module["logger"]({type:"stdout",message:message})}
function printErr (line 8) | function printErr(message){if(!message.startsWith("Aborted(native code c...
function exec (line 8) | function exec(..._args){const args=[...Module["DEFAULT_ARGS"],..._args];...
function ffprobe (line 8) | function ffprobe(..._args){const args=[...Module["DEFAULT_ARGS_FFPROBE"]...
function setLogger (line 8) | function setLogger(logger){Module["logger"]=logger}
function setTimeout (line 8) | function setTimeout(timeout){Module["timeout"]=timeout}
function setProgress (line 8) | function setProgress(handler){Module["progress"]=handler}
function receiveProgress (line 8) | function receiveProgress(progress,time){Module["progress"]({progress:pro...
function reset (line 8) | function reset(){Module["ret"]=-1;Module["timeout"]=-1}
function _locateFile (line 8) | function _locateFile(path,prefix){const mainScriptUrlOrBlob=Module["main...
function locateFile (line 8) | function locateFile(path){if(Module["locateFile"]){return Module["locate...
function assert (line 8) | function assert(condition,text){if(!condition){abort(text)}}
function updateMemoryViews (line 8) | function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEA...
function keepRuntimeAlive (line 8) | function keepRuntimeAlive(){return noExitRuntime||runtimeKeepaliveCounte...
function preRun (line 8) | function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="func...
function initRuntime (line 8) | function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!...
function postRun (line 8) | function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="f...
function addOnPreRun (line 8) | function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}
function addOnInit (line 8) | function addOnInit(cb){__ATINIT__.unshift(cb)}
function addOnPostRun (line 8) | function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}
function getUniqueRunDependency (line 8) | function getUniqueRunDependency(id){return id}
function addRunDependency (line 8) | function addRunDependency(id){runDependencies++;if(Module["monitorRunDep...
function removeRunDependency (line 8) | function removeRunDependency(id){runDependencies--;if(Module["monitorRun...
function abort (line 8) | function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what=...
function isDataURI (line 8) | function isDataURI(filename){return filename.startsWith(dataURIPrefix)}
function getBinary (line 8) | function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return...
function getBinaryPromise (line 8) | function getBinaryPromise(binaryFile){if(!wasmBinary&&(ENVIRONMENT_IS_WE...
function instantiateArrayBuffer (line 8) | function instantiateArrayBuffer(binaryFile,imports,receiver){return getB...
function instantiateAsync (line 8) | function instantiateAsync(binary,binaryFile,imports,callback){if(!binary...
function createWasm (line 8) | function createWasm(){var info={"a":wasmImports};function receiveInstanc...
function send_progress (line 8) | function send_progress(progress,time){Module.receiveProgress(progress,ti...
function is_timeout (line 8) | function is_timeout(diff){if(Module.timeout===-1)return 0;else{return Mo...
function ExitStatus (line 8) | function ExitStatus(status){this.name="ExitStatus";this.message=`Program...
function callRuntimeCallbacks (line 8) | function callRuntimeCallbacks(callbacks){while(callbacks.length>0){callb...
function getWasmTableEntry (line 8) | function getWasmTableEntry(funcPtr){var func=wasmTableMirror[funcPtr];if...
function getValue (line 8) | function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(t...
function setValue (line 8) | function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";sw...
function UTF8ArrayToString (line 8) | function UTF8ArrayToString(heapOrArray,idx,maxBytesToRead){var endIdx=id...
function UTF8ToString (line 8) | function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(H...
function ___assert_fail (line 8) | function ___assert_fail(condition,filename,line,func){abort(`Assertion f...
function ExceptionInfo (line 8) | function ExceptionInfo(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24;thi...
function ___cxa_throw (line 8) | function ___cxa_throw(ptr,type,destructor){var info=new ExceptionInfo(pt...
function ___dlsym (line 8) | function ___dlsym(handle,symbol){abort(dlopenMissingError)}
function initRandomFill (line 8) | function initRandomFill(){if(typeof crypto=="object"&&typeof crypto["get...
function randomFill (line 8) | function randomFill(view){return(randomFill=initRandomFill())(view)}
function trim (line 8) | function trim(arr){var start=0;for(;start<arr.length;start++){if(arr[sta...
function lengthBytesUTF8 (line 8) | function lengthBytesUTF8(str){var len=0;for(var i=0;i<str.length;++i){va...
function stringToUTF8Array (line 8) | function stringToUTF8Array(str,heap,outIdx,maxBytesToWrite){if(!(maxByte...
function intArrayFromString (line 8) | function intArrayFromString(stringy,dontAddNull,length){var len=length>0...
function zeroMemory (line 8) | function zeroMemory(address,size){HEAPU8.fill(0,address,address+size);re...
function alignMemory (line 8) | function alignMemory(size,alignment){return Math.ceil(size/alignment)*al...
function mmapAlloc (line 8) | function mmapAlloc(size){size=alignMemory(size,65536);var ptr=_emscripte...
function asyncLoad (line 8) | function asyncLoad(url,onload,onerror,noRunDep){var dep=!noRunDep?getUni...
function FS_handledByPreloadPlugin (line 8) | function FS_handledByPreloadPlugin(byteArray,fullname,finish,onerror){if...
function FS_createPreloadedFile (line 8) | function FS_createPreloadedFile(parent,name,url,canRead,canWrite,onload,...
function FS_modeStringToFlags (line 8) | function FS_modeStringToFlags(str){var flagModes={"r":0,"r+":2,"w":512|6...
function FS_getMode (line 8) | function FS_getMode(canRead,canWrite){var mode=0;if(canRead)mode|=292|73...
function ensureParent (line 8) | function ensureParent(path){var parts=path.split("/");var parent=root;fo...
function base (line 8) | function base(path){var parts=path.split("/");return parts[parts.length-1]}
function doCallback (line 8) | function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}
function done (line 8) | function done(errCode){if(errCode){if(!done.errored){done.errored=true;r...
function LazyUint8Array (line 8) | function LazyUint8Array(){this.lengthKnown=false;this.chunks=[]}
function writeChunks (line 8) | function writeChunks(stream,buffer,offset,length,position){var contents=...
function ___syscall__newselect (line 8) | function ___syscall__newselect(nfds,readfds,writefds,exceptfds,timeout){...
function handleMessage (line 8) | function handleMessage(data){if(typeof data=="string"){var encoder=new T...
function getSocketFromFD (line 8) | function getSocketFromFD(fd){var socket=SOCKFS.getSocket(fd);if(!socket)...
function setErrNo (line 8) | function setErrNo(value){HEAP32[___errno_location()>>2]=value;return value}
function inetPton4 (line 8) | function inetPton4(str){var b=str.split(".");for(var i=0;i<4;i++){var tm...
function jstoi_q (line 8) | function jstoi_q(str){return parseInt(str)}
function inetPton6 (line 8) | function inetPton6(str){var words;var w,offset,z;var valid6regx=/^((?=.*...
function writeSockaddr (line 8) | function writeSockaddr(sa,family,addr,port,addrlen){switch(family){case ...
function ___syscall_accept4 (line 8) | function ___syscall_accept4(fd,addr,addrlen,flags,d1,d2){try{var sock=ge...
function inetNtop4 (line 8) | function inetNtop4(addr){return(addr&255)+"."+(addr>>8&255)+"."+(addr>>1...
function inetNtop6 (line 8) | function inetNtop6(ints){var str="";var word=0;var longest=0;var lastzer...
function readSockaddr (line 8) | function readSockaddr(sa,salen){var family=HEAP16[sa>>1];var port=_ntohs...
function getSocketAddress (line 8) | function getSocketAddress(addrp,addrlen,allowNull){if(allowNull&&addrp==...
function ___syscall_bind (line 8) | function ___syscall_bind(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocke...
function ___syscall_connect (line 8) | function ___syscall_connect(fd,addr,addrlen,d1,d2,d3){try{var sock=getSo...
function ___syscall_faccessat (line 8) | function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS....
function ___syscall_fcntl64 (line 8) | function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try...
function ___syscall_fstat64 (line 8) | function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFro...
function stringToUTF8 (line 8) | function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Arr...
function ___syscall_getdents64 (line 8) | function ___syscall_getdents64(fd,dirp,count){try{var stream=SYSCALLS.ge...
function ___syscall_getpeername (line 8) | function ___syscall_getpeername(fd,addr,addrlen,d1,d2,d3){try{var sock=g...
function ___syscall_getsockname (line 8) | function ___syscall_getsockname(fd,addr,addrlen,d1,d2,d3){try{var sock=g...
function ___syscall_getsockopt (line 8) | function ___syscall_getsockopt(fd,level,optname,optval,optlen,d1){try{va...
function ___syscall_ioctl (line 8) | function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{va...
function ___syscall_listen (line 8) | function ___syscall_listen(fd,backlog){try{var sock=getSocketFromFD(fd);...
function ___syscall_lstat64 (line 8) | function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);ret...
function ___syscall_mkdirat (line 8) | function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(pa...
function ___syscall_newfstatat (line 8) | function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.g...
function ___syscall_openat (line 8) | function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=va...
function ___syscall_poll (line 8) | function ___syscall_poll(fds,nfds,timeout){try{var nonzero=0;for(var i=0...
function ___syscall_recvfrom (line 8) | function ___syscall_recvfrom(fd,buf,len,flags,addr,addrlen){try{var sock...
function ___syscall_renameat (line 8) | function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldp...
function ___syscall_rmdir (line 8) | function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(...
function ___syscall_sendto (line 8) | function ___syscall_sendto(fd,message,length,flags,addr,addr_len){try{va...
function ___syscall_socket (line 8) | function ___syscall_socket(domain,type,protocol){try{var sock=SOCKFS.cre...
function ___syscall_stat64 (line 8) | function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);retu...
function ___syscall_unlinkat (line 8) | function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(...
function __emscripten_get_now_is_monotonic (line 8) | function __emscripten_get_now_is_monotonic(){return nowIsMonotonic}
function __emscripten_throw_longjmp (line 8) | function __emscripten_throw_longjmp(){throw Infinity}
function readI53FromI64 (line 8) | function readI53FromI64(ptr){return HEAPU32[ptr>>2]+HEAP32[ptr+4>>2]*429...
function __gmtime_js (line 8) | function __gmtime_js(time,tmPtr){var date=new Date(readI53FromI64(time)*...
function isLeapYear (line 8) | function isLeapYear(year){return year%4===0&&(year%100!==0||year%400===0)}
function ydayFromDate (line 8) | function ydayFromDate(date){var leap=isLeapYear(date.getFullYear());var ...
function __localtime_js (line 8) | function __localtime_js(time,tmPtr){var date=new Date(readI53FromI64(tim...
function __mktime_js (line 8) | function __mktime_js(tmPtr){var date=new Date(HEAP32[tmPtr+20>>2]+1900,H...
function __mmap_js (line 8) | function __mmap_js(len,prot,flags,fd,off,allocated,addr){try{var stream=...
function __munmap_js (line 8) | function __munmap_js(addr,len,prot,flags,fd,offset){try{var stream=SYSCA...
function stringToNewUTF8 (line 8) | function stringToNewUTF8(str){var size=lengthBytesUTF8(str)+1;var ret=_m...
function __tzset_js (line 8) | function __tzset_js(timezone,daylight,tzname){var currentYear=(new Date)...
function _abort (line 8) | function _abort(){abort("")}
function _dlopen (line 8) | function _dlopen(handle){abort(dlopenMissingError)}
function readEmAsmArgs (line 8) | function readEmAsmArgs(sigPtr,buf){readEmAsmArgsArray.length=0;var ch;bu...
function runEmAsmFunction (line 8) | function runEmAsmFunction(code,sigPtr,argbuf){var args=readEmAsmArgs(sig...
function _emscripten_asm_const_int (line 8) | function _emscripten_asm_const_int(code,sigPtr,argbuf){return runEmAsmFu...
function _emscripten_date_now (line 8) | function _emscripten_date_now(){return Date.now()}
function getHeapMax (line 8) | function getHeapMax(){return 2147483648}
function _emscripten_get_heap_max (line 8) | function _emscripten_get_heap_max(){return getHeapMax()}
function _emscripten_memcpy_big (line 8) | function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src...
function emscripten_realloc_buffer (line 8) | function emscripten_realloc_buffer(size){var b=wasmMemory.buffer;try{was...
function _emscripten_resize_heap (line 8) | function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.lengt...
function getExecutableName (line 8) | function getExecutableName(){return thisProgram||"./this.program"}
function getEnvStrings (line 8) | function getEnvStrings(){if(!getEnvStrings.strings){var lang=(typeof nav...
function stringToAscii (line 8) | function stringToAscii(str,buffer){for(var i=0;i<str.length;++i){HEAP8[b...
function _environ_get (line 8) | function _environ_get(__environ,environ_buf){var bufSize=0;getEnvStrings...
function _environ_sizes_get (line 8) | function _environ_sizes_get(penviron_count,penviron_buf_size){var string...
function _proc_exit (line 8) | function _proc_exit(code){EXITSTATUS=code;if(!keepRuntimeAlive()){if(Mod...
function exitJS (line 8) | function exitJS(status,implicit){EXITSTATUS=status;_proc_exit(status)}
function _fd_close (line 8) | function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.cl...
function _fd_fdstat_get (line 8) | function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriti...
function doReadv (line 8) | function doReadv(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i<iovcn...
function _fd_read (line 8) | function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamF...
function bigintToI53Checked (line 8) | function bigintToI53Checked(num){return num<MIN_INT53||num>MAX_INT53?NaN...
function _fd_seek (line 8) | function _fd_seek(fd,offset,whence,newOffset){try{offset=bigintToI53Chec...
function doWritev (line 8) | function doWritev(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i<iovc...
function _fd_write (line 8) | function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStream...
function _getaddrinfo (line 8) | function _getaddrinfo(node,service,hint,out){var addr=0;var port=0;var f...
function _getnameinfo (line 8) | function _getnameinfo(sa,salen,node,nodelen,serv,servlen,flags){var info...
function arraySum (line 8) | function arraySum(array,index){var sum=0;for(var i=0;i<=index;sum+=array...
function addDays (line 8) | function addDays(date,days){var newDate=new Date(date.getTime());while(d...
function writeArrayToMemory (line 8) | function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}
function _strftime (line 8) | function _strftime(s,maxsize,format,tm){var tm_zone=HEAP32[tm+40>>2];var...
function invoke_iiiii (line 8) | function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return g...
function invoke_vii (line 8) | function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntr...
function invoke_iii (line 8) | function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTa...
function invoke_iiiijj (line 8) | function invoke_iiiijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{retu...
function invoke_iiiiiiiii (line 8) | function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSav...
function invoke_vi (line 8) | function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(in...
function invoke_viiii (line 8) | function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmT...
function invoke_iiii (line 8) | function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWa...
function invoke_iij (line 8) | function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTa...
function invoke_i (line 8) | function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry...
function invoke_ii (line 8) | function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableE...
function invoke_viiiiiiii (line 8) | function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSav...
function invoke_iiiiii (line 8) | function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{retu...
function invoke_viiiiii (line 8) | function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{...
function run (line 8) | function run(){if(runDependencies>0){return}preRun();if(runDependencies>...
FILE: web/src/api/ai.ts
method list (line 11) | list(serviceType?: AIServiceType) {
method create (line 17) | create(data: CreateAIConfigRequest) {
method get (line 21) | get(id: number) {
method update (line 25) | update(id: number, data: UpdateAIConfigRequest) {
method delete (line 29) | delete(id: number) {
method testConnection (line 33) | testConnection(data: TestConnectionRequest) {
FILE: web/src/api/asset.ts
method createAsset (line 12) | createAsset(data: CreateAssetRequest) {
method updateAsset (line 16) | updateAsset(id: number, data: UpdateAssetRequest) {
method getAsset (line 20) | getAsset(id: number) {
method listAssets (line 24) | listAssets(params: ListAssetsParams) {
method deleteAsset (line 36) | deleteAsset(id: number) {
method importFromImage (line 40) | importFromImage(imageGenId: number) {
method importFromVideo (line 44) | importFromVideo(videoGenId: number) {
FILE: web/src/api/audio.ts
constant API_BASE_URL (line 3) | const API_BASE_URL = '/api/v1'
type ExtractAudioRequest (line 5) | interface ExtractAudioRequest {
type ExtractAudioResponse (line 9) | interface ExtractAudioResponse {
type BatchExtractAudioRequest (line 14) | interface BatchExtractAudioRequest {
type BatchExtractAudioResponse (line 18) | interface BatchExtractAudioResponse {
FILE: web/src/api/character-library.ts
type CharacterLibraryItem (line 3) | interface CharacterLibraryItem {
type CreateLibraryItemRequest (line 15) | interface CreateLibraryItemRequest {
type CharacterLibraryQuery (line 24) | interface CharacterLibraryQuery {
method list (line 34) | list(params?: CharacterLibraryQuery) {
method create (line 47) | create(data: CreateLibraryItemRequest) {
method get (line 52) | get(id: string) {
method delete (line 57) | delete(id: string) {
method uploadCharacterImage (line 62) | uploadCharacterImage(characterId: string, imageUrl: string) {
method applyFromLibrary (line 67) | applyFromLibrary(characterId: string, libraryItemId: string) {
method addCharacterToLibrary (line 74) | addCharacterToLibrary(characterId: string, category?: string) {
method generateCharacterImage (line 81) | generateCharacterImage(characterId: string, model?: string) {
method batchGenerateCharacterImages (line 88) | batchGenerateCharacterImages(characterIds: string[], model?: string) {
method updateCharacter (line 96) | updateCharacter(characterId: number, data: {
method deleteCharacter (line 108) | deleteCharacter(characterId: number) {
method extractFromEpisode (line 113) | extractFromEpisode(episodeId: number) {
FILE: web/src/api/drama.ts
method list (line 11) | list(params?: DramaListQuery) {
method create (line 23) | create(data: CreateDramaRequest) {
method get (line 27) | get(id: string) {
method update (line 31) | update(id: string, data: UpdateDramaRequest) {
method delete (line 35) | delete(id: string) {
method getStats (line 39) | getStats() {
method saveOutline (line 43) | saveOutline(id: string, data: { title: string; summary: string; genre?: ...
method getCharacters (line 47) | getCharacters(dramaId: string) {
method saveCharacters (line 51) | saveCharacters(id: string, data: any[], episodeId?: string) {
method updateCharacter (line 58) | updateCharacter(id: number, data: any) {
method saveEpisodes (line 62) | saveEpisodes(id: string, data: any[]) {
method saveProgress (line 66) | saveProgress(id: string, data: { current_step: string; step_data?: any }) {
method generateStoryboard (line 70) | generateStoryboard(episodeId: string) {
method getBackgrounds (line 74) | getBackgrounds(episodeId: string) {
method extractBackgrounds (line 78) | extractBackgrounds(episodeId: string, model?: string) {
method batchGenerateBackgrounds (line 82) | batchGenerateBackgrounds(episodeId: string) {
method generateSingleBackground (line 86) | generateSingleBackground(backgroundId: number, dramaId: string, prompt: ...
method getStoryboards (line 94) | getStoryboards(episodeId: string) {
method updateStoryboard (line 98) | updateStoryboard(storyboardId: string, data: any) {
method updateScene (line 102) | updateScene(sceneId: string, data: {
method createScene (line 118) | createScene(data: {
method generateSceneImage (line 131) | generateSceneImage(data: { scene_id: number; prompt?: string; model?: st...
method updateScenePrompt (line 135) | updateScenePrompt(sceneId: string, prompt: string) {
method deleteScene (line 139) | deleteScene(sceneId: string) {
method finalizeEpisode (line 144) | finalizeEpisode(episodeId: string, timelineData?: any) {
method createStoryboard (line 148) | createStoryboard(data: {
method deleteStoryboard (line 161) | deleteStoryboard(storyboardId: number) {
FILE: web/src/api/frame.ts
type FrameType (line 4) | type FrameType = 'first' | 'key' | 'last' | 'panel' | 'action'
type SingleFramePrompt (line 7) | interface SingleFramePrompt {
type MultiFramePrompt (line 13) | interface MultiFramePrompt {
type GenerateFramePromptResponse (line 19) | interface GenerateFramePromptResponse {
type GenerateFramePromptRequest (line 26) | interface GenerateFramePromptRequest {
function generateFramePrompt (line 34) | function generateFramePrompt(
function generateFirstFrame (line 44) | function generateFirstFrame(storyboardId: number): Promise<GenerateFrame...
function generateKeyFrame (line 51) | function generateKeyFrame(storyboardId: number): Promise<GenerateFramePr...
function generateLastFrame (line 58) | function generateLastFrame(storyboardId: number): Promise<GenerateFrameP...
function generatePanelFrames (line 65) | function generatePanelFrames(
function generateActionSequence (line 78) | function generateActionSequence(storyboardId: number): Promise<GenerateF...
type FramePromptRecord (line 83) | interface FramePromptRecord {
function getStoryboardFramePrompts (line 97) | function getStoryboardFramePrompts(storyboardId: number): Promise<{ fram...
FILE: web/src/api/generation.ts
method generateCharacters (line 7) | generateCharacters(data: GenerateCharactersRequest) {
method generateStoryboard (line 11) | generateStoryboard(episodeId: string, model?: string) {
method getTaskStatus (line 15) | getTaskStatus(taskId: string) {
FILE: web/src/api/image.ts
method generateImage (line 9) | generateImage(data: GenerateImageRequest) {
method generateForScene (line 13) | generateForScene(sceneId: number) {
method batchGenerateForEpisode (line 17) | batchGenerateForEpisode(episodeId: number) {
method getImage (line 21) | getImage(id: number) {
method listImages (line 25) | listImages(params: ImageGenerationListParams) {
method deleteImage (line 37) | deleteImage(id: number) {
method uploadImage (line 42) | uploadImage(data: {
FILE: web/src/api/prop.ts
method list (line 5) | list(dramaId: string | number) {
method create (line 8) | create(data: CreatePropRequest) {
method update (line 11) | update(id: number, data: UpdatePropRequest) {
method delete (line 14) | delete(id: number) {
method extractFromScript (line 17) | extractFromScript(episodeId: number) {
method generateImage (line 20) | generateImage(id: number) {
method associateWithStoryboard (line 23) | associateWithStoryboard(storyboardId: number, propIds: number[]) {
FILE: web/src/api/settings.ts
method getLanguage (line 5) | getLanguage() {
method updateLanguage (line 10) | updateLanguage(language: 'zh' | 'en') {
FILE: web/src/api/task.ts
type AsyncTask (line 3) | interface AsyncTask {
method getStatus (line 15) | getStatus(taskId: string) {
FILE: web/src/api/video.ts
method generateVideo (line 9) | generateVideo(data: GenerateVideoRequest) {
method generateFromImage (line 13) | generateFromImage(imageGenId: number) {
method batchGenerateForEpisode (line 17) | batchGenerateForEpisode(episodeId: number) {
method getVideoGeneration (line 21) | getVideoGeneration(id: number) {
method getVideo (line 25) | getVideo(id: number) {
method listVideos (line 29) | listVideos(params: VideoGenerationListParams) {
method deleteVideo (line 41) | deleteVideo(id: number) {
FILE: web/src/api/videoMerge.ts
type SceneClip (line 3) | interface SceneClip {
type MergeVideoRequest (line 12) | interface MergeVideoRequest {
type VideoMerge (line 21) | interface VideoMerge {
method mergeVideos (line 39) | async mergeVideos(data: MergeVideoRequest): Promise<VideoMerge> {
method getMerge (line 44) | async getMerge(mergeId: number): Promise<VideoMerge> {
method listMerges (line 49) | async listMerges(params: {
method deleteMerge (line 62) | async deleteMerge(mergeId: number): Promise<void> {
FILE: web/src/stores/episode.ts
type EpisodeCache (line 6) | interface EpisodeCache {
type EpisodeOperations (line 13) | interface EpisodeOperations {
type SetOperationParams (line 23) | interface SetOperationParams {
type DeleteOperationParams (line 28) | interface DeleteOperationParams {
type GenerateImageOptions (line 33) | interface GenerateImageOptions {
type CachedEpisode (line 38) | interface CachedEpisode {
method refresh (line 68) | async refresh() {
method set (line 72) | async set(params: SetOperationParams) {
method del (line 91) | async del(params: DeleteOperationParams) {
method saveScript (line 107) | async saveScript(content: string) {
method extractData (line 120) | async extractData() {
method generateImages (line 125) | async generateImages(options?: GenerateImageOptions) {
method generateStoryboards (line 161) | async generateStoryboards() {
method value (line 168) | get value() {
method loading (line 171) | get loading() {
method error (line 174) | get error() {
FILE: web/src/types/ai.ts
type AIServiceConfig (line 1) | interface AIServiceConfig {
type AIServiceType (line 18) | type AIServiceType = 'text' | 'image' | 'video'
type CreateAIConfigRequest (line 20) | interface CreateAIConfigRequest {
type UpdateAIConfigRequest (line 33) | interface UpdateAIConfigRequest {
type TestConnectionRequest (line 46) | interface TestConnectionRequest {
type AIServiceProvider (line 55) | interface AIServiceProvider {
FILE: web/src/types/asset.ts
type Asset (line 1) | interface Asset {
type AssetType (line 30) | type AssetType = 'image' | 'video' | 'audio'
type AssetTag (line 32) | interface AssetTag {
type AssetCollection (line 39) | interface AssetCollection {
type CreateAssetRequest (line 48) | interface CreateAssetRequest {
type UpdateAssetRequest (line 68) | interface UpdateAssetRequest {
type ListAssetsParams (line 77) | interface ListAssetsParams {
constant ASSET_CATEGORIES (line 90) | const ASSET_CATEGORIES = {
FILE: web/src/types/drama.ts
type Drama (line 3) | interface Drama {
type DramaStatus (line 26) | type DramaStatus = 'draft' | 'planning' | 'production' | 'completed' | '...
type Character (line 28) | interface Character {
type Episode (line 49) | interface Episode {
type Storyboard (line 74) | interface Storyboard {
type Scene (line 99) | interface Scene {
type CreateDramaRequest (line 119) | interface CreateDramaRequest {
type UpdateDramaRequest (line 127) | interface UpdateDramaRequest {
type DramaListQuery (line 136) | interface DramaListQuery {
type DramaStats (line 144) | interface DramaStats {
FILE: web/src/types/generation.ts
type GenerateCharactersRequest (line 1) | interface GenerateCharactersRequest {
type ParseScriptRequest (line 10) | interface ParseScriptRequest {
type ParseScriptResult (line 16) | interface ParseScriptResult {
type ParsedCharacter (line 22) | interface ParsedCharacter {
type ParsedEpisode (line 29) | interface ParsedEpisode {
FILE: web/src/types/image.ts
type ImageGeneration (line 1) | interface ImageGeneration {
type ImageStatus (line 32) | type ImageStatus = 'pending' | 'processing' | 'completed' | 'failed'
type ImageProvider (line 34) | type ImageProvider = 'openai' | 'dalle' | 'midjourney' | 'stable_diffusi...
type GenerateImageRequest (line 36) | interface GenerateImageRequest {
type ImageGenerationListParams (line 57) | interface ImageGenerationListParams {
FILE: web/src/types/prop.ts
type Prop (line 1) | interface Prop {
type CreatePropRequest (line 14) | interface CreatePropRequest {
type UpdatePropRequest (line 23) | interface UpdatePropRequest {
FILE: web/src/types/timeline.ts
type Timeline (line 3) | interface Timeline {
type TimelineStatus (line 18) | type TimelineStatus = 'draft' | 'editing' | 'completed' | 'exporting'
type TimelineTrack (line 20) | interface TimelineTrack {
type TrackType (line 33) | type TrackType = 'video' | 'audio' | 'text'
type TimelineClip (line 35) | interface TimelineClip {
type ClipTransition (line 60) | interface ClipTransition {
type TransitionType (line 68) | type TransitionType = 'fade' | 'crossfade' | 'slide' | 'wipe' | 'zoom' |...
type ClipEffect (line 70) | interface ClipEffect {
type EffectType (line 80) | type EffectType = 'filter' | 'color' | 'blur' | 'brightness' | 'contrast...
type CreateTimelineRequest (line 82) | interface CreateTimelineRequest {
type UpdateTimelineRequest (line 91) | interface UpdateTimelineRequest {
type CreateTrackRequest (line 99) | interface CreateTrackRequest {
type UpdateTrackRequest (line 106) | interface UpdateTrackRequest {
type CreateClipRequest (line 114) | interface CreateClipRequest {
type UpdateClipRequest (line 129) | interface UpdateClipRequest {
type CreateTransitionRequest (line 142) | interface CreateTransitionRequest {
constant TRANSITION_TYPES (line 149) | const TRANSITION_TYPES = [
constant EFFECT_TYPES (line 158) | const EFFECT_TYPES = [
FILE: web/src/types/user.ts
type User (line 1) | interface User {
type UserConfig (line 13) | interface UserConfig {
FILE: web/src/types/video.ts
type VideoGeneration (line 1) | interface VideoGeneration {
type VideoStatus (line 32) | type VideoStatus = 'pending' | 'processing' | 'completed' | 'failed'
type VideoProvider (line 34) | type VideoProvider = 'runway' | 'pika' | 'doubao' | 'openai'
type GenerateVideoRequest (line 36) | interface GenerateVideoRequest {
type VideoGenerationListParams (line 58) | interface VideoGenerationListParams {
constant VIDEO_ASPECT_RATIOS (line 67) | const VIDEO_ASPECT_RATIOS = [
constant CAMERA_MOTIONS (line 74) | const CAMERA_MOTIONS = [
FILE: web/src/utils/ffmpeg.ts
type VideoTrimOptions (line 7) | interface VideoTrimOptions {
type VideoMergeOptions (line 12) | interface VideoMergeOptions {
type ProgressCallback (line 20) | interface ProgressCallback {
function getFFmpeg (line 24) | async function getFFmpeg(): Promise<FFmpeg> {
function trimVideo (line 53) | async function trimVideo(
function mergeVideos (line 92) | async function mergeVideos(
function trimAndMergeVideos (line 142) | async function trimAndMergeVideos(
function isFFmpegLoaded (line 208) | async function isFFmpegLoaded(): Promise<boolean> {
function unloadFFmpeg (line 212) | async function unloadFFmpeg(): Promise<void> {
FILE: web/src/utils/image.ts
function fixImageUrl (line 8) | function fixImageUrl(url: string): string {
function getImageUrl (line 19) | function getImageUrl(item: any): string {
function hasImage (line 39) | function hasImage(item: any): boolean {
function getVideoUrl (line 48) | function getVideoUrl(item: any): string {
function hasVideo (line 77) | function hasVideo(item: any): boolean {
FILE: web/src/utils/request.ts
type CustomAxiosInstance (line 5) | interface CustomAxiosInstance extends Omit<AxiosInstance, 'get' | 'post'...
FILE: web/src/utils/videoMerger.ts
type VideoClip (line 4) | interface VideoClip {
type TransitionType (line 12) | type TransitionType = 'fade' | 'fadeblack' | 'fadewhite' | 'slideleft' |...
type TransitionEffect (line 14) | interface TransitionEffect {
type MergeProgress (line 19) | interface MergeProgress {
class VideoMerger (line 25) | class VideoMerger {
method constructor (line 30) | constructor() {
method initialize (line 34) | async initialize(onProgress?: (progress: MergeProgress) => void) {
method mergeVideos (line 117) | async mergeVideos(clips: VideoClip[]): Promise<Blob> {
method mergeWithTransitions (line 250) | private async mergeWithTransitions(inputFiles: string[], clips: VideoC...
method getXfadeFilter (line 301) | private getXfadeFilter(type: TransitionType, duration: number, offset:...
method terminate (line 320) | async terminate() {
Condensed preview — 183 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,809K chars).
[
{
"path": ".dockerignore",
"chars": 429,
"preview": "# Git\n.git\n.gitignore\n.gitattributes\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# 构建产物\ndist/\nbuild/\n"
},
{
"path": ".gitignore",
"chars": 754,
"preview": "# Binaries\nbin/\ndrama-generator\nbackend\n*.exe\n*.dll\n*.so\n*.dylib\ndrama-generator.exe\n\n# Test binary\n*.test\n\n# Output of "
},
{
"path": "DOCKER_HOST_ACCESS.md",
"chars": 1309,
"preview": "# Docker 容器访问宿主机服务指南\n\n## 核心配置\n\nDocker 容器内使用 `http://host.docker.internal:端口号` 访问宿主机服务。\n\n### macOS / Windows\n\n直接使用,无需额外配置"
},
{
"path": "Dockerfile",
"chars": 2792,
"preview": "# 多阶段构建 Dockerfile for Huobao Drama\n\n# ==================== 阶段1: 构建前端 ====================\n# 声明构建参数(支持镜像源配置)\nARG DOCKER_"
},
{
"path": "LICENSE",
"chars": 1484,
"preview": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License\n\nCopyright (c) 2026 火宝 (Chatfire)"
},
{
"path": "MIGRATE_README.md",
"chars": 2133,
"preview": "# 数据清洗工具使用说明\n\n## 使用方法\n\n### 本地部署\n\n```bash\n# 在项目根目录执行\ngo run cmd/migrate/main.go\n```\n\n### Docker 部署\n\n在 Docker 容器中,迁移脚本已经被编"
},
{
"path": "README-CN.md",
"chars": 12006,
"preview": "# 🎬 Huobao Drama - AI 短剧生成平台\n\n<div align=\"center\">\n\n**基于 Go + Vue3 的全栈 AI 短剧自动化生产平台**\n\n[\n\n// BatchGen"
},
{
"path": "api/handlers/character_library.go",
"chars": 7336,
"preview": "package handlers\n\nimport (\n\t\"strconv\"\n\n\tservices2 \"github.com/drama-generator/backend/application/services\"\n\t\"github.com"
},
{
"path": "api/handlers/character_library_gen.go",
"chars": 899,
"preview": "package handlers\n\nimport (\n\t\"github.com/drama-generator/backend/pkg/response\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Generate"
},
{
"path": "api/handlers/drama.go",
"chars": 8100,
"preview": "package handlers\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/dra"
},
{
"path": "api/handlers/frame_prompt.go",
"chars": 1589,
"preview": "package handlers\n\nimport (\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-generator/backe"
},
{
"path": "api/handlers/frame_prompt_query.go",
"chars": 818,
"preview": "package handlers\n\nimport (\n\t\"github.com/drama-generator/backend/domain/models\"\n\t\"github.com/drama-generator/backend/pkg/"
},
{
"path": "api/handlers/image_generation.go",
"chars": 6444,
"preview": "package handlers\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gen"
},
{
"path": "api/handlers/prop.go",
"chars": 3960,
"preview": "package handlers\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gen"
},
{
"path": "api/handlers/scene.go",
"chars": 3571,
"preview": "package handlers\n\nimport (\n\tservices2 \"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gener"
},
{
"path": "api/handlers/script_generation.go",
"chars": 1322,
"preview": "package handlers\n\nimport (\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-generator/backe"
},
{
"path": "api/handlers/settings.go",
"chars": 1452,
"preview": "package handlers\n\nimport (\n\t\"github.com/drama-generator/backend/pkg/config\"\n\t\"github.com/drama-generator/backend/pkg/log"
},
{
"path": "api/handlers/storyboard.go",
"chars": 2894,
"preview": "package handlers\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gen"
},
{
"path": "api/handlers/task.go",
"chars": 1354,
"preview": "package handlers\n\nimport (\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-generator/backe"
},
{
"path": "api/handlers/upload.go",
"chars": 3538,
"preview": "package handlers\n\nimport (\n\tservices2 \"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gener"
},
{
"path": "api/handlers/video_generation.go",
"chars": 3958,
"preview": "package handlers\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gen"
},
{
"path": "api/handlers/video_merge.go",
"chars": 2718,
"preview": "package handlers\n\nimport (\n\t\"strconv\"\n\n\tservices2 \"github.com/drama-generator/backend/application/services\"\n\t\"github.com"
},
{
"path": "api/middlewares/cors.go",
"chars": 1356,
"preview": "package middlewares\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc CORSMiddleware(allowedOrigins []string) gin.HandlerFun"
},
{
"path": "api/middlewares/logger.go",
"chars": 580,
"preview": "package middlewares\n\nimport (\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/pkg/logger\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nf"
},
{
"path": "api/middlewares/ratelimit.go",
"chars": 975,
"preview": "package middlewares\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/pkg/response\"\n\t\"github.com/gin-gonic"
},
{
"path": "api/routes/routes.go",
"chars": 9704,
"preview": "package routes\n\nimport (\n\thandlers2 \"github.com/drama-generator/backend/api/handlers\"\n\tmiddlewares2 \"github.com/drama-ge"
},
{
"path": "application/services/ai_service.go",
"chars": 12650,
"preview": "package services\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/drama-generator/backend/domain/models\"\n\t\"github.com/drama-gene"
},
{
"path": "application/services/asset_duration_update.go",
"chars": 1759,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\n\tmodels \"github.com/drama-generator/backend/domain/models\"\n\t\"github.com/drama-generat"
},
{
"path": "application/services/asset_service.go",
"chars": 8176,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\tmodels \"github.com/drama-generator/backend/domain/models\"\n\t\"gi"
},
{
"path": "application/services/audio_extraction_service.go",
"chars": 2851,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/infrastructure/external"
},
{
"path": "application/services/character_library_service.go",
"chars": 17828,
"preview": "package services\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tmodels \"github.com/drama-generator/backend/domain/models\"\n\t\"github"
},
{
"path": "application/services/data_migration_service.go",
"chars": 14660,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/drama-genera"
},
{
"path": "application/services/drama_service.go",
"chars": 19303,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/do"
},
{
"path": "application/services/frame_prompt_helper.go",
"chars": 914,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// parseFramePromptJSON 解析AI返回的JSON格式提示词\nfunc (s *Fr"
},
{
"path": "application/services/frame_prompt_service.go",
"chars": 17327,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/drama-generator/backend/domain/models\"\n\t\"github.com/drama-gen"
},
{
"path": "application/services/image_generation_service.go",
"chars": 42120,
"preview": "package services\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"tim"
},
{
"path": "application/services/prompt_i18n.go",
"chars": 47226,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/drama-generator/backend/pkg/config\"\n)\n\n// PromptI18n 提示词国际化工具\ntype Promp"
},
{
"path": "application/services/prop_service.go",
"chars": 7112,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t// Added missing import\n\tmodels \"github.com/drama-generator/backend/domain/m"
},
{
"path": "application/services/resource_transfer_service.go",
"chars": 395,
"preview": "package services\n\nimport (\n\t\"github.com/drama-generator/backend/pkg/logger\"\n\t\"gorm.io/gorm\"\n)\n\ntype ResourceTransferServ"
},
{
"path": "application/services/script_generation_service.go",
"chars": 6609,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/drama-generator/backend/domain/models\"\n\t\"github.com/drama-gen"
},
{
"path": "application/services/storyboard_composition_service.go",
"chars": 16441,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\tmodels \"github.com/drama-generator/backend/domain/models\"\n\t\"github."
},
{
"path": "application/services/storyboard_service.go",
"chars": 28566,
"preview": "package services\n\nimport (\n\t\"strconv\"\n\n\t\"fmt\"\n\t\"strings\"\n\n\tmodels \"github.com/drama-generator/backend/domain/models\"\n\t\"g"
},
{
"path": "application/services/storyboard_update_full.go",
"chars": 3897,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/drama-generator/backend/domain/models\"\n)\n\n// UpdateStoryboard 更新分镜的所有字段,"
},
{
"path": "application/services/task_service.go",
"chars": 2700,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/domain/models\"\n\t\"github"
},
{
"path": "application/services/upload_service.go",
"chars": 3426,
"preview": "package services\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/pkg/config\""
},
{
"path": "application/services/video_generation_service.go",
"chars": 27771,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tmodels \"github.com/drama-generator/ba"
},
{
"path": "application/services/video_merge_service.go",
"chars": 19722,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"time\"\n\n\tmodels \"github.co"
},
{
"path": "cmd/migrate/main.go",
"chars": 15011,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/drama-generator/"
},
{
"path": "configs/config.example.yaml",
"chars": 536,
"preview": "app:\n name: \"Huobao Drama API\"\n version: \"1.0.0\"\n debug: true\n language: \"zh\" # 系统语言:zh(中文) 或 en(英文)\n\nserver:\n port"
},
{
"path": "docker-compose.yml",
"chars": 1286,
"preview": "services:\n huobao-drama:\n # image: huobao-drama:latest\n container_name: huobao-drama\n build:\n context: .\n"
},
{
"path": "docs/DATA_MIGRATION.md",
"chars": 3232,
"preview": "# 数据清洗服务文档\n\n## 概述\n\n数据清洗服务(Data Migration Service)用于自动下载并迁移数据库中 `local_path` 字段为空的数据。该服务会在应用启动时自动执行,将远程 URL 的文件下载到本地存储,并更"
},
{
"path": "domain/models/ai_config.go",
"chars": 3595,
"preview": "package models\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"time\"\n)\n\ntype AIServiceConfig struct {\n\tID "
},
{
"path": "domain/models/asset.go",
"chars": 2064,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype Asset struct {\n\tID uint `gorm:\"primarykey\" js"
},
{
"path": "domain/models/character_library.go",
"chars": 1034,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n)\n\n// CharacterLibrary 角色库模型\ntype CharacterLibrary struct {\n\tID "
},
{
"path": "domain/models/drama.go",
"chars": 9385,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/datatypes\"\n\t\"gorm.io/gorm\"\n)\n\ntype Drama struct {\n\tID uint "
},
{
"path": "domain/models/frame_prompt.go",
"chars": 967,
"preview": "package models\n\nimport \"time\"\n\n// FramePrompt 帧提示词存储表\ntype FramePrompt struct {\n\tID uint `gorm:\"primaryke"
},
{
"path": "domain/models/image_generation.go",
"chars": 3704,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/datatypes\"\n)\n\ntype ImageGeneration struct {\n\tID uint "
},
{
"path": "domain/models/task.go",
"chars": 1072,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n)\n\n// AsyncTask 异步任务模型\ntype AsyncTask struct {\n\tID string "
},
{
"path": "domain/models/timeline.go",
"chars": 6013,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype Timeline struct {\n\tID uint `gorm:\"primarykey\""
},
{
"path": "domain/models/video_generation.go",
"chars": 3057,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype VideoGeneration struct {\n\tID uint `gorm:\"prim"
},
{
"path": "domain/models/video_merge.go",
"chars": 2113,
"preview": "package models\n\nimport (\n\t\"time\"\n\n\t\"gorm.io/datatypes\"\n\t\"gorm.io/gorm\"\n)\n\ntype VideoMergeStatus string\n\nconst (\n\tVideoMe"
},
{
"path": "go.mod",
"chars": 3295,
"preview": "module github.com/drama-generator/backend\n\ngo 1.23.0\n\nreplace github.com/drama-generator/backend => ./\n\nrequire (\n\tgithu"
},
{
"path": "go.sum",
"chars": 63953,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
},
{
"path": "infrastructure/database/custom_logger.go",
"chars": 2730,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gorm.io/gorm/logger\"\n)\n\n// CustomLogger 自定义 GORM logger,截断过长"
},
{
"path": "infrastructure/database/database.go",
"chars": 2070,
"preview": "package database\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/domain/models\"\n\t\""
},
{
"path": "infrastructure/external/ffmpeg/ffmpeg.go",
"chars": 23002,
"preview": "package ffmpeg\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dra"
},
{
"path": "infrastructure/scheduler/resource_transfer_scheduler.go",
"chars": 6047,
"preview": "package scheduler\n\nimport (\n\t\"time\"\n\n\t\"github.com/drama-generator/backend/application/services\"\n\t\"github.com/drama-gener"
},
{
"path": "infrastructure/storage/local_storage.go",
"chars": 4522,
"preview": "package storage\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n"
},
{
"path": "main.go",
"chars": 3158,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/drama-ge"
},
{
"path": "migrations/20260126_add_local_path.sql",
"chars": 471,
"preview": "-- 添加 local_path 字段到相关表\n-- 创建时间: 2026-01-26\n-- 说明: 为 characters, scenes, props, character_libraries 表添加 local_path 字段以支持"
},
{
"path": "migrations/init.sql",
"chars": 19198,
"preview": "-- AI短剧生成平台 - SQLite数据库初始化脚本 (开源版本 - 无用户认证)\n-- 创建时间: 2026-01-07\n-- 说明: 此版本适配SQLite,移除外键约束,适合单机部署\n\n-- ==================="
},
{
"path": "pkg/ai/client.go",
"chars": 263,
"preview": "package ai\n\n// AIClient 定义文本生成客户端接口\ntype AIClient interface {\n\tGenerateText(prompt string, systemPrompt string, options "
},
{
"path": "pkg/ai/gemini_client.go",
"chars": 5370,
"preview": "package ai\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype GeminiClient struct {"
},
{
"path": "pkg/ai/openai_client.go",
"chars": 9724,
"preview": "package ai\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype OpenAIClient struct {"
},
{
"path": "pkg/config/config.go",
"chars": 2426,
"preview": "package config\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/viper\"\n)\n\ntype Config struct {\n\tApp AppConfig `mapstructur"
},
{
"path": "pkg/image/gemini_image_client.go",
"chars": 6640,
"preview": "package image\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype"
},
{
"path": "pkg/image/image_client.go",
"chars": 1723,
"preview": "package image\n\ntype ImageClient interface {\n\tGenerateImage(prompt string, opts ...ImageOption) (*ImageResult, error)\n\tGe"
},
{
"path": "pkg/image/openai_image_client.go",
"chars": 2927,
"preview": "package image\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype OpenAIImageClient struct {\n\tB"
},
{
"path": "pkg/image/volcengine_image_client.go",
"chars": 4008,
"preview": "package image\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype VolcEngineImageClient struct "
},
{
"path": "pkg/logger/logger.go",
"chars": 683,
"preview": "package logger\n\nimport (\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\ntype Logger struct {\n\t*zap.SugaredLogger\n}\n\nfu"
},
{
"path": "pkg/response/response.go",
"chars": 2922,
"preview": "package response\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype Response struct {\n\tSuccess bool "
},
{
"path": "pkg/utils/image_utils.go",
"chars": 1897,
"preview": "package utils\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n)\n\n// ImageToBase64 将图片转换为 base64 编"
},
{
"path": "pkg/utils/json_parser.go",
"chars": 5702,
"preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// SafeParseAIJSON 安全地解析AI返回的JSON,处理常见的格式问题\n// 包"
},
{
"path": "pkg/utils/json_parser_test.go",
"chars": 2978,
"preview": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\n// TestAttemptJSONRepairExcessBraces tests fixing JSON with exces"
},
{
"path": "pkg/video/chatfire_client.go",
"chars": 11307,
"preview": "package video\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\n// ChatfireClient Chatf"
},
{
"path": "pkg/video/minimax_client.go",
"chars": 7839,
"preview": "package video\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// MiniMax Hailuo 支持的模型\nconst (\n\t/"
},
{
"path": "pkg/video/openai_sora_client.go",
"chars": 7258,
"preview": "package video\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/te"
},
{
"path": "pkg/video/video_client.go",
"chars": 9482,
"preview": "package video\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype VideoClient interface {\n\tGene"
},
{
"path": "pkg/video/volces_ark_client.go",
"chars": 7869,
"preview": "package video\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\n// VolcesArkClient 火山引擎"
},
{
"path": "web/.gitignore",
"chars": 297,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "web/index.html",
"chars": 387,
"preview": "<!doctype html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href"
},
{
"path": "web/nginx.conf",
"chars": 643,
"preview": "server {\n listen 80;\n server_name localhost;\n root /usr/share/nginx/html;\n index index.html;\n\n location /"
},
{
"path": "web/package.json",
"chars": 1121,
"preview": "{\n \"name\": \"drama-generator-frontend\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n "
},
{
"path": "web/public/ffmpeg/ffmpeg-core.js",
"chars": 112059,
"preview": "\nvar createFFmpegCore = (() => {\n var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document"
},
{
"path": "web/src/App.vue",
"chars": 135,
"preview": "<template>\n <router-view />\n</template>\n\n<script setup lang=\"ts\"></script>\n\n<style>\n#app {\n width: 100%;\n height: 100"
},
{
"path": "web/src/api/ai.ts",
"chars": 872,
"preview": "import type {\n AIServiceConfig,\n AIServiceType,\n CreateAIConfigRequest,\n TestConnectionRequest,\n UpdateAI"
},
{
"path": "web/src/api/asset.ts",
"chars": 1061,
"preview": "import type {\n Asset,\n AssetCollection,\n AssetTag,\n CreateAssetRequest,\n ListAssetsParams,\n UpdateAsse"
},
{
"path": "web/src/api/audio.ts",
"chars": 969,
"preview": "import axios from 'axios'\n\nconst API_BASE_URL = '/api/v1'\n\nexport interface ExtractAudioRequest {\n video_url: string\n}\n"
},
{
"path": "web/src/api/character-library.ts",
"chars": 2793,
"preview": "import request from '../utils/request'\n\nexport interface CharacterLibraryItem {\n id: string\n name: string\n category?:"
},
{
"path": "web/src/api/drama.ts",
"chars": 4226,
"preview": "import type {\n CreateDramaRequest,\n Drama,\n DramaListQuery,\n DramaStats,\n UpdateDramaRequest\n} from '../types/drama"
},
{
"path": "web/src/api/frame.ts",
"chars": 2315,
"preview": "import request from '../utils/request'\n\n// 帧类型\nexport type FrameType = 'first' | 'key' | 'last' | 'panel' | 'action'\n\n//"
},
{
"path": "web/src/api/generation.ts",
"chars": 826,
"preview": "import type {\n GenerateCharactersRequest\n} from '../types/generation'\nimport request from '../utils/request'\n\nexport "
},
{
"path": "web/src/api/image.ts",
"chars": 1197,
"preview": "import type {\n GenerateImageRequest,\n ImageGeneration,\n ImageGenerationListParams\n} from '../types/image'\nimport requ"
},
{
"path": "web/src/api/prop.ts",
"chars": 974,
"preview": "import request from '../utils/request'\nimport type { Prop, CreatePropRequest, UpdatePropRequest } from '../types/prop'\n\n"
},
{
"path": "web/src/api/settings.ts",
"chars": 330,
"preview": "import request from '../utils/request'\n\nexport const settingsAPI = {\n // 获取系统语言\n getLanguage() {\n return request.ge"
},
{
"path": "web/src/api/task.ts",
"chars": 389,
"preview": "import request from '../utils/request'\n\nexport interface AsyncTask {\n id: string\n type: string\n status: 'pendin"
},
{
"path": "web/src/api/video.ts",
"chars": 1071,
"preview": "import type {\n GenerateVideoRequest,\n VideoGeneration,\n VideoGenerationListParams\n} from '../types/video'\nimport requ"
},
{
"path": "web/src/api/videoMerge.ts",
"chars": 1572,
"preview": "import request from '../utils/request'\n\nexport interface SceneClip {\n scene_id: string\n video_url: string\n start_time"
},
{
"path": "web/src/assets/styles/element/index.scss",
"chars": 355,
"preview": "/*just override what you need*/\n@forward 'element-plus/theme-chalk/src/dark/var.scss' with (\n $bg-color: (\n 'page': "
},
{
"path": "web/src/assets/styles/main.css",
"chars": 24747,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* ========================================\n CSS Variables"
},
{
"path": "web/src/components/LanguageSwitcher.vue",
"chars": 3445,
"preview": "<template>\n <el-dropdown @command=\"handleCommand\">\n <span class=\"language-switcher\">\n <el-icon><Switch /></el-i"
},
{
"path": "web/src/components/common/AIConfigDialog.vue",
"chars": 24282,
"preview": "<template>\n <el-dialog\n v-model=\"visible\"\n :title=\"$t('aiConfig.title')\"\n width=\"900px\"\n :close-on-click-mo"
},
{
"path": "web/src/components/common/ActionButton.vue",
"chars": 1967,
"preview": "<template>\n <!-- Minimalist action button with icon and optional tooltip -->\n <!-- 简约操作按钮,带图标和可选提示 -->\n <el-tooltip\n "
},
{
"path": "web/src/components/common/AppHeader.vue",
"chars": 5816,
"preview": "<template>\n <div class=\"app-header-wrapper\">\n <header class=\"app-header\" :class=\"{ 'header-fixed': fixed }\">\n <"
},
{
"path": "web/src/components/common/AppLayout.vue",
"chars": 2761,
"preview": "<template>\n <div class=\"app-layout\">\n <!-- Global Header -->\n <header class=\"app-header\">\n <div class=\"heade"
},
{
"path": "web/src/components/common/BaseCard.vue",
"chars": 4287,
"preview": "<template>\n <!-- Base Card Component - Reusable card with modern design -->\n <!-- 基础卡片组件 - 现代设计的可复用卡片 -->\n <div \n "
},
{
"path": "web/src/components/common/CreateDramaDialog.vue",
"chars": 6785,
"preview": "<template>\n <!-- Create Drama Dialog / 创建短剧弹窗 -->\n <el-dialog\n v-model=\"visible\"\n :title=\"$t('drama.createNew')\""
},
{
"path": "web/src/components/common/EmptyState.vue",
"chars": 2617,
"preview": "<template>\n <!-- Empty State Component - Display when no data available -->\n <!-- 空状态组件 - 无数据时的展示 -->\n <div :class=\"["
},
{
"path": "web/src/components/common/ImageCropDialog.vue",
"chars": 18063,
"preview": "<template>\n <el-dialog\n v-model=\"dialogVisible\"\n title=\"裁剪动作序列图\"\n width=\"70vw\"\n :close-on-click-modal=\"fals"
},
{
"path": "web/src/components/common/ImagePreview.vue",
"chars": 3462,
"preview": "<template>\n <div class=\"image-preview-wrapper\">\n <!-- 缩略图 -->\n <div\n class=\"thumbnail-container\"\n @clic"
},
{
"path": "web/src/components/common/PageHeader.vue",
"chars": 4439,
"preview": "<template>\n <!-- Page header component with title, subtitle and action buttons -->\n <!-- 页面头部组件,包含标题、副标题和操作按钮 -->\n <h"
},
{
"path": "web/src/components/common/ProjectCard.vue",
"chars": 3887,
"preview": "<template>\n <!-- Project card component - Compact design with hover actions -->\n <!-- 项目卡片组件 - 紧凑设计,悬停显示操作 -->\n <arti"
},
{
"path": "web/src/components/common/StatCard.vue",
"chars": 3955,
"preview": "<template>\n <!-- Stat Card Component - Display statistics with modern design -->\n <!-- 统计卡片组件 - 现代设计的统计数据展示 -->\n <div"
},
{
"path": "web/src/components/common/ThemeToggle.vue",
"chars": 2284,
"preview": "<template>\n <!-- Theme toggle button for switching between light/dark mode -->\n <!-- 主题切换按钮,用于切换浅色/深色模式 -->\n <button\n"
},
{
"path": "web/src/components/common/index.ts",
"chars": 957,
"preview": "/**\n * Common UI Components barrel export\n * 通用 UI 组件统一导出\n */\n\n// Layout Components / 布局组件\nexport { default as PageHeade"
},
{
"path": "web/src/components/editor/GridImageEditor.vue",
"chars": 13370,
"preview": "<template>\n <el-dialog v-model=\"visible\" :title=\"$t('editor.gridImageEditor')\" width=\"900px\" :close-on-click-modal=\"fal"
},
{
"path": "web/src/components/editor/StoryboardEditor.vue",
"chars": 43638,
"preview": "<template>\n <div class=\"storyboard-editor\">\n <!-- 左侧分镜列表 -->\n <div class=\"left-panel\">\n <div class=\"panel-he"
},
{
"path": "web/src/components/editor/VideoTimelineEditor.vue",
"chars": 76121,
"preview": "<template>\n <div class=\"video-timeline-editor\">\n <!-- 顶部工具栏 -->\n <div class=\"editor-toolbar\">\n <div class=\"t"
},
{
"path": "web/src/locales/en-US.ts",
"chars": 28383,
"preview": "export default {\n nav: {\n home: 'Home',\n characters: 'Characters',\n storyboard: 'Storyboard',\n videos: 'Vid"
},
{
"path": "web/src/locales/index.ts",
"chars": 817,
"preview": "import { createI18n } from 'vue-i18n'\nimport zhCN from './zh-CN'\nimport enUS from './en-US'\n\n// 从 localStorage 获取保存的语言,默"
},
{
"path": "web/src/locales/zh-CN.ts",
"chars": 18636,
"preview": "export default {\n nav: {\n home: '首页',\n characters: '角色管理',\n storyboard: '分镜制作',\n videos: '视频管理',\n assets"
},
{
"path": "web/src/main.ts",
"chars": 878,
"preview": "import { createApp } from 'vue'\nimport { createPinia } from 'pinia'\nimport ElementPlus from 'element-plus'\nimport 'eleme"
},
{
"path": "web/src/router/index.ts",
"chars": 2080,
"preview": "import type { RouteRecordRaw } from 'vue-router'\nimport { createRouter, createWebHistory } from 'vue-router'\n\nconst rout"
},
{
"path": "web/src/stores/episode.ts",
"chars": 6393,
"preview": "import { ref, computed, reactive } from 'vue'\nimport { defineStore } from 'pinia'\nimport { dramaAPI } from '@/api/drama'"
},
{
"path": "web/src/types/ai.ts",
"chars": 1532,
"preview": "export interface AIServiceConfig {\n id: number\n service_type: AIServiceType\n provider?: string // 厂商标识\n name: strin"
},
{
"path": "web/src/types/asset.ts",
"chars": 1856,
"preview": "export interface Asset {\n id: number\n drama_id?: number\n episode_id?: number\n storyboard_id?: number\n storyboard_nu"
},
{
"path": "web/src/types/drama.ts",
"chars": 2912,
"preview": "import { Prop } from './prop'\n\nexport interface Drama {\n id: string\n\n title: string\n description?: string\n genre?: s"
},
{
"path": "web/src/types/generation.ts",
"chars": 753,
"preview": "export interface GenerateCharactersRequest {\n drama_id: string\n episode_id?: number\n outline?: string\n count?: numbe"
},
{
"path": "web/src/types/image.ts",
"chars": 1364,
"preview": "export interface ImageGeneration {\n id: number\n storyboard_id?: number\n scene_id?: string\n drama_id: string\n charac"
},
{
"path": "web/src/types/prop.ts",
"chars": 546,
"preview": "export interface Prop {\n id: number\n drama_id: number\n name: string\n type?: string\n description?: string\n"
},
{
"path": "web/src/types/timeline.ts",
"chars": 3368,
"preview": "import type { Asset } from './asset'\n\nexport interface Timeline {\n id: number\n drama_id: number\n episode_id?: number\n"
},
{
"path": "web/src/types/user.ts",
"chars": 515,
"preview": "export interface User {\n id: number\n username: string\n email: string\n avatar?: string\n nickname?: string\n phone?: "
},
{
"path": "web/src/types/video.ts",
"chars": 2086,
"preview": "export interface VideoGeneration {\n id: number\n storyboard_id?: number\n scene_id?: string // 已废弃,保留用于兼容\n drama_id: "
},
{
"path": "web/src/utils/ffmpeg.ts",
"chars": 5006,
"preview": "import { FFmpeg } from '@ffmpeg/ffmpeg'\nimport { fetchFile, toBlobURL } from '@ffmpeg/util'\n\nlet ffmpegInstance: FFmpeg "
},
{
"path": "web/src/utils/image.ts",
"chars": 1542,
"preview": "/**\n * 图片URL工具函数\n */\n\n/**\n * 修复图片URL,处理相对路径和绝对路径\n */\nexport function fixImageUrl(url: string): string {\n if (!url) retu"
},
{
"path": "web/src/utils/request.ts",
"chars": 1431,
"preview": "import type { AxiosError, AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'\nimport axios from"
},
{
"path": "web/src/utils/videoMerger.ts",
"chars": 8964,
"preview": "import { FFmpeg } from '@ffmpeg/ffmpeg'\nimport { fetchFile, toBlobURL } from '@ffmpeg/util'\n\nexport interface VideoClip "
},
{
"path": "web/src/views/dashboard/Dashboard.vue",
"chars": 4368,
"preview": "<template>\n <div class=\"dashboard-container\">\n <el-container>\n <el-header class=\"header\">\n <div class=\"h"
},
{
"path": "web/src/views/drama/DramaCreate.vue",
"chars": 4567,
"preview": "<template>\n <!-- Drama Create Page / 创建短剧页面 -->\n <div class=\"page-container\">\n <div class=\"content-wrapper animate-"
},
{
"path": "web/src/views/drama/DramaList.vue",
"chars": 14106,
"preview": "<template>\n <!-- Drama List Page - Refactored with modern minimalist design -->\n <!-- 短剧列表页面 - 使用现代简约设计重构 -->\n <div c"
},
{
"path": "web/src/views/drama/DramaManagement.vue",
"chars": 52046,
"preview": "<template>\n <div class=\"page-container\">\n <div class=\"content-wrapper animate-fade-in\">\n <!-- Page Header / 页面头"
},
{
"path": "web/src/views/drama/DramaWorkflow.vue",
"chars": 56297,
"preview": "<template>\n <div class=\"workflow-container\">\n <AppHeader :fixed=\"false\" :show-logo=\"false\">\n <template #left>\n "
},
{
"path": "web/src/views/drama/EpisodeWorkflow.vue",
"chars": 88052,
"preview": "<template>\n <div class=\"page-container\">\n <div class=\"content-wrapper animate-fade-in\">\n <AppHeader :fixed=\"fal"
},
{
"path": "web/src/views/drama/ProfessionalEditor.vue",
"chars": 180198,
"preview": "<template>\n <div class=\"professional-editor\">\n <!-- 顶部工具栏 -->\n <AppHeader\n :fixed=\"false\"\n :show-logo=\""
},
{
"path": "web/src/views/drama/components/UploadScriptDialog.vue",
"chars": 5466,
"preview": "<template>\n <el-dialog\n v-model=\"visible\"\n title=\"上传剧本\"\n width=\"800px\"\n :close-on-click-modal=\"false\"\n @"
},
{
"path": "web/src/views/editor/TimelineEditor.vue",
"chars": 1959,
"preview": "<template>\n <div class=\"timeline-editor-page\">\n <div class=\"editor-header\">\n <el-button link @click=\"goBack\" cl"
},
{
"path": "web/src/views/generation/ImageGeneration.vue",
"chars": 10717,
"preview": "<template>\n <div class=\"image-generation-container\">\n <el-page-header @back=\"goBack\" class=\"page-header\">\n <tem"
},
{
"path": "web/src/views/generation/VideoGeneration.vue",
"chars": 11582,
"preview": "<template>\n <div class=\"video-generation-container\">\n <el-page-header @back=\"goBack\" class=\"page-header\">\n <tem"
},
{
"path": "web/src/views/generation/components/GenerateImageDialog.vue",
"chars": 8679,
"preview": "<template>\n <el-dialog\n v-model=\"visible\"\n:title=\"$t('imageDialog.title')\"\n width=\"700px\"\n :close-on-click-mod"
},
{
"path": "web/src/views/generation/components/GenerateVideoDialog.vue",
"chars": 9793,
"preview": "<template>\n <el-dialog\n v-model=\"visible\"\n title=\"AI 视频生成\"\n width=\"700px\"\n :close-on-click-modal=\"false\"\n "
},
{
"path": "web/src/views/generation/components/ImageDetailDialog.vue",
"chars": 7051,
"preview": "<template>\n <el-dialog\n v-model=\"visible\"\n title=\"图片详情\"\n width=\"900px\"\n @close=\"handleClose\"\n >\n <div v"
},
{
"path": "web/src/views/generation/components/VideoDetailDialog.vue",
"chars": 7588,
"preview": "<template>\n <el-dialog\n v-model=\"visible\"\n title=\"视频详情\"\n width=\"1000px\"\n @close=\"handleClose\"\n >\n <div "
},
{
"path": "web/src/views/script/ScriptEdit.vue",
"chars": 439,
"preview": "<template>\n <div class=\"script-edit-container\">\n <el-page-header @back=\"goBack\" title=\"返回\">\n <template #content"
},
{
"path": "web/src/views/settings/AIConfig.vue",
"chars": 20540,
"preview": "<template>\n <div class=\"page-container\">\n <div class=\"content-wrapper animate-fade-in\">\n <!-- Page Header / 页面头"
},
{
"path": "web/src/views/settings/SystemSettings.vue",
"chars": 4665,
"preview": "<template>\n <div class=\"system-settings\">\n <el-card>\n <template #header>\n <div class=\"card-header\">\n "
},
{
"path": "web/src/views/settings/components/ConfigList.vue",
"chars": 5421,
"preview": "<template>\n <div v-loading=\"loading\" class=\"config-list\">\n <el-empty v-if=\"!loading && configs.length === 0\" :descri"
},
{
"path": "web/src/views/storyboard/StoryboardEdit.vue",
"chars": 514,
"preview": "<template>\n <div class=\"storyboard-edit-container\">\n <el-page-header @back=\"goBack\" :title=\"$t('common.back')\">\n "
},
{
"path": "web/src/views/workflow/CharacterExtraction.vue",
"chars": 5673,
"preview": "<template>\n <div class=\"character-extraction-container\">\n <el-page-header @back=\"goBack\" :title=\"$t('character.backT"
},
{
"path": "web/src/views/workflow/CharacterImages.vue",
"chars": 9560,
"preview": "<template>\n <div class=\"character-images-container\">\n <el-page-header @back=\"goBack\" title=\"返回项目\">\n <template #"
},
{
"path": "web/src/views/workflow/DramaSettings.vue",
"chars": 3668,
"preview": "<template>\n <div class=\"drama-settings-container\">\n <el-page-header @back=\"goBack\" title=\"返回项目\">\n <template #co"
},
{
"path": "web/src/views/workflow/SceneImages.vue",
"chars": 4937,
"preview": "<template>\n <div class=\"scene-images-container\">\n <el-page-header @back=\"goBack\" title=\"返回项目\">\n <template #cont"
},
{
"path": "web/src/views/workflow/StoryboardGeneration.vue",
"chars": 331,
"preview": "<template>\n <div class=\"storyboard-generation\">\n <el-empty description=\"分镜拆解功能开发中\" />\n </div>\n</template>\n\n<script "
},
{
"path": "web/tailwind.config.js",
"chars": 1821,
"preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\n \"./index.html\",\n \"./src/**/*.{vue,js,ts"
},
{
"path": "web/tsconfig.json",
"chars": 765,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\":"
},
{
"path": "web/tsconfig.node.json",
"chars": 236,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\""
},
{
"path": "web/vite.config.ts",
"chars": 530,
"preview": "import vue from '@vitejs/plugin-vue'\nimport { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vite'\n\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the chatfire-AI/huobao-drama GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 183 files (32.4 MB), approximately 489.7k tokens, and a symbol index with 1198 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.