diff --git a/api/handlers/image_generation.go b/api/handlers/image_generation.go index c389c99..7ba5dbf 100644 --- a/api/handlers/image_generation.go +++ b/api/handlers/image_generation.go @@ -14,12 +14,14 @@ import ( type ImageGenerationHandler struct { imageService *services.ImageGenerationService + taskService *services.TaskService log *logger.Logger } func NewImageGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services.ResourceTransferService, localStorage *storage.LocalStorage) *ImageGenerationHandler { return &ImageGenerationHandler{ imageService: services.NewImageGenerationService(db, transferService, localStorage, log), + taskService: services.NewTaskService(db, log), log: log, } } @@ -71,18 +73,57 @@ func (h *ImageGenerationHandler) GetBackgroundsForEpisode(c *gin.Context) { } func (h *ImageGenerationHandler) ExtractBackgroundsForEpisode(c *gin.Context) { - episodeID := c.Param("episode_id") - // 同步执行场景提取 - backgrounds, err := h.imageService.ExtractBackgroundsForEpisode(episodeID) + // 创建异步任务 + task, err := h.taskService.CreateTask("background_extraction", episodeID) if err != nil { - h.log.Errorw("Failed to extract backgrounds", "error", err) + h.log.Errorw("Failed to create task", "error", err) response.InternalError(c, err.Error()) return } - response.Success(c, backgrounds) + // 启动后台goroutine处理 + go h.processBackgroundExtraction(task.ID, episodeID) + + // 立即返回任务ID + response.Success(c, gin.H{ + "task_id": task.ID, + "status": "pending", + "message": "场景提取任务已创建,正在后台处理...", + }) +} + +// processBackgroundExtraction 后台处理场景提取 +func (h *ImageGenerationHandler) processBackgroundExtraction(taskID, episodeID string) { + h.log.Infow("Starting background extraction", "task_id", taskID, "episode_id", episodeID) + + // 更新任务状态为处理中 + if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始提取场景..."); err != nil { + h.log.Errorw("Failed to update task status", "error", err) + } + + // 调用实际的提取逻辑 + backgrounds, err := h.imageService.ExtractBackgroundsForEpisode(episodeID) + if err != nil { + h.log.Errorw("Failed to extract backgrounds", "error", err, "task_id", taskID) + if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil { + h.log.Errorw("Failed to update task error", "error", updateErr) + } + return + } + + // 更新任务结果 + result := gin.H{ + "backgrounds": backgrounds, + "total": len(backgrounds), + } + if err := h.taskService.UpdateTaskResult(taskID, result); err != nil { + h.log.Errorw("Failed to update task result", "error", err) + return + } + + h.log.Infow("Background extraction completed", "task_id", taskID, "total", len(backgrounds)) } func (h *ImageGenerationHandler) BatchGenerateForEpisode(c *gin.Context) { diff --git a/api/handlers/script_generation.go b/api/handlers/script_generation.go index 1d5fd60..6264959 100644 --- a/api/handlers/script_generation.go +++ b/api/handlers/script_generation.go @@ -11,12 +11,14 @@ import ( 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, log), + taskService: services.NewTaskService(db, log), log: log, } } @@ -46,15 +48,55 @@ func (h *ScriptGenerationHandler) GenerateCharacters(c *gin.Context) { return } - // 同步执行角色生成 - characters, err := h.scriptService.GenerateCharacters(&req) + // 创建异步任务 + task, err := h.taskService.CreateTask("character_generation", req.DramaID) if err != nil { - h.log.Errorw("Failed to generate characters", "error", err) - response.InternalError(c, "生成角色失败") + h.log.Errorw("Failed to create task", "error", err) + response.InternalError(c, err.Error()) return } - response.Success(c, characters) + // 启动后台goroutine处理 + go h.processCharacterGeneration(task.ID, &req) + + // 立即返回任务ID + response.Success(c, gin.H{ + "task_id": task.ID, + "status": "pending", + "message": "角色生成任务已创建,正在后台处理...", + }) +} + +// processCharacterGeneration 后台处理角色生成 +func (h *ScriptGenerationHandler) processCharacterGeneration(taskID string, req *services.GenerateCharactersRequest) { + h.log.Infow("Starting character generation", "task_id", taskID, "drama_id", req.DramaID) + + // 更新任务状态为处理中 + if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始生成角色..."); err != nil { + h.log.Errorw("Failed to update task status", "error", err) + } + + // 调用实际的生成逻辑 + characters, err := h.scriptService.GenerateCharacters(req) + if err != nil { + h.log.Errorw("Failed to generate characters", "error", err, "task_id", taskID) + if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil { + h.log.Errorw("Failed to update task error", "error", updateErr) + } + return + } + + // 更新任务结果 + result := gin.H{ + "characters": characters, + "total": len(characters), + } + if err := h.taskService.UpdateTaskResult(taskID, result); err != nil { + h.log.Errorw("Failed to update task result", "error", err) + return + } + + h.log.Infow("Character generation completed", "task_id", taskID, "total", len(characters)) } func (h *ScriptGenerationHandler) GenerateEpisodes(c *gin.Context) { diff --git a/application/services/ai_service.go b/application/services/ai_service.go index c1802c3..2dc868e 100644 --- a/application/services/ai_service.go +++ b/application/services/ai_service.go @@ -317,14 +317,14 @@ func (s *AIService) TestConnection(req *TestConnectionRequest) error { func (s *AIService) GetDefaultConfig(serviceType string) (*models.AIServiceConfig, error) { var config models.AIServiceConfig - // 按优先级降序获取第一个启用的配置 - err := s.db.Where("service_type = ? AND is_active = ?", serviceType, true). + // 按优先级降序获取第一个配置 + err := s.db.Where("service_type = ?", serviceType). 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, errors.New("no config found") } return nil, err } @@ -332,10 +332,10 @@ func (s *AIService) GetDefaultConfig(serviceType string) (*models.AIServiceConfi return &config, nil } -// GetConfigForModel 根据服务类型和模型名称获取优先级最高的启用配置 +// 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). + err := s.db.Where("service_type = ?", serviceType). Order("priority DESC, created_at DESC"). Find(&configs).Error diff --git a/pkg/video/chatfire_client.go b/pkg/video/chatfire_client.go index d0e17a6..f8a9cb9 100644 --- a/pkg/video/chatfire_client.go +++ b/pkg/video/chatfire_client.go @@ -41,9 +41,10 @@ type ChatfireSoraRequest struct { type ChatfireDoubaoRequest struct { Model string `json:"model"` Content []struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - URL string `json:"url,omitempty"` + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` } `json:"content"` } @@ -70,6 +71,9 @@ type ChatfireTaskResponse struct { Status string `json:"status,omitempty"` VideoURL string `json:"video_url,omitempty"` } `json:"data,omitempty"` + Content struct { + VideoURL string `json:"video_url,omitempty"` + } `json:"content,omitempty"` } // getErrorMessage 从 error 字段提取错误信息(支持字符串或对象) @@ -144,18 +148,83 @@ func (c *ChatfireClient) GenerateVideo(imageURL, prompt string, opts ...VideoOpt } // 添加文本内容 reqBody.Content = append(reqBody.Content, struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - URL string `json:"url,omitempty"` + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` }{Type: "text", Text: prompt}) - // 如果有图片URL,添加图片内容 - if imageURL != "" { + // 处理不同的图片模式 + // 1. 组图模式(多个reference_image) + if len(options.ReferenceImageURLs) > 0 { + for _, refURL := range options.ReferenceImageURLs { + reqBody.Content = append(reqBody.Content, struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` + }{ + Type: "image_url", + ImageURL: map[string]interface{}{ + "url": refURL, + }, + Role: "reference_image", + }) + } + } else if options.FirstFrameURL != "" && options.LastFrameURL != "" { + // 2. 首尾帧模式 reqBody.Content = append(reqBody.Content, struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - URL string `json:"url,omitempty"` - }{Type: "image_url", URL: imageURL}) + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` + }{ + Type: "image_url", + ImageURL: map[string]interface{}{ + "url": options.FirstFrameURL, + }, + Role: "first_frame", + }) + reqBody.Content = append(reqBody.Content, struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` + }{ + Type: "image_url", + ImageURL: map[string]interface{}{ + "url": options.LastFrameURL, + }, + Role: "last_frame", + }) + } else if imageURL != "" { + // 3. 单图模式(默认) + reqBody.Content = append(reqBody.Content, struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` + }{ + Type: "image_url", + ImageURL: map[string]interface{}{ + "url": imageURL, + }, + // 单图模式不需要role + }) + } else if options.FirstFrameURL != "" { + // 4. 只有首帧 + reqBody.Content = append(reqBody.Content, struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]interface{} `json:"image_url,omitempty"` + Role string `json:"role,omitempty"` + }{ + Type: "image_url", + ImageURL: map[string]interface{}{ + "url": options.FirstFrameURL, + }, + Role: "first_frame", + }) } jsonData, err = json.Marshal(reqBody) @@ -286,6 +355,9 @@ func (c *ChatfireClient) GetTaskStatus(taskID string) (*VideoResult, error) { return nil, fmt.Errorf("read response: %w", err) } + // 调试日志:打印响应内容 + fmt.Printf("[Chatfire] GetTaskStatus Response body: %s\n", string(body)) + var result ChatfireTaskResponse if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("parse response: %w, body: %s", err, string(body)) @@ -307,10 +379,16 @@ func (c *ChatfireClient) GetTaskStatus(taskID string) (*VideoResult, error) { status = result.Data.Status } + // 按优先级获取 video_url:VideoURL -> Data.VideoURL -> Content.VideoURL videoURL := result.VideoURL if videoURL == "" && result.Data.VideoURL != "" { videoURL = result.Data.VideoURL } + if videoURL == "" && result.Content.VideoURL != "" { + videoURL = result.Content.VideoURL + } + + fmt.Printf("[Chatfire] Parsed result - TaskID: %s, Status: %s, VideoURL: %s\n", responseTaskID, status, videoURL) videoResult := &VideoResult{ TaskID: responseTaskID, diff --git a/web/package.json b/web/package.json index ff5f333..650033f 100644 --- a/web/package.json +++ b/web/package.json @@ -20,6 +20,7 @@ "element-plus": "^2.5.0", "pinia": "^2.1.0", "vue": "^3.4.0", + "vue-i18n": "^9.14.5", "vue-router": "^4.2.0" }, "devDependencies": { diff --git a/web/src/api/drama.ts b/web/src/api/drama.ts index af335ca..53dccfe 100644 --- a/web/src/api/drama.ts +++ b/web/src/api/drama.ts @@ -72,7 +72,7 @@ export const dramaAPI = { }, extractBackgrounds(episodeId: string) { - return request.post(`/images/episode/${episodeId}/backgrounds/extract`) + return request.post<{ task_id: string; status: string; message: string }>(`/images/episode/${episodeId}/backgrounds/extract`) }, batchGenerateBackgrounds(episodeId: string) { diff --git a/web/src/api/generation.ts b/web/src/api/generation.ts index 49edd6a..7a2fac9 100644 --- a/web/src/api/generation.ts +++ b/web/src/api/generation.ts @@ -13,7 +13,7 @@ export const generationAPI = { }, generateCharacters(data: GenerateCharactersRequest) { - return request.post('/generation/characters', data) + return request.post<{ task_id: string; status: string; message: string }>('/generation/characters', data) }, generateEpisodes(data: GenerateEpisodesRequest) { diff --git a/web/src/components/LanguageSwitcher.vue b/web/src/components/LanguageSwitcher.vue new file mode 100644 index 0000000..b002948 --- /dev/null +++ b/web/src/components/LanguageSwitcher.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/web/src/components/editor/VideoTimelineEditor.vue b/web/src/components/editor/VideoTimelineEditor.vue index 87acc5a..88c5eaa 100644 --- a/web/src/components/editor/VideoTimelineEditor.vue +++ b/web/src/components/editor/VideoTimelineEditor.vue @@ -4,8 +4,8 @@
- 播放 - 暂停 + {{ $t('common.play') }} + {{ $t('common.pause') }} {{ formatTime(currentTime) }} / {{ formatTime(totalDuration) }}
@@ -17,7 +17,7 @@ :disabled="timelineClips.length === 0" :loading="serverMerging" > - 合成视频 + {{ $t('video.merge') }}
@@ -42,7 +42,7 @@ :style="{ animationDuration: transitionState.duration + 's' }" >
- +
@@ -59,8 +59,8 @@
-

视频素材库

- {{ availableStoryboards.length }} 个视频 +

{{ $t('video.mediaLibrary') }}

+ {{ $t('video.videoCount', { count: availableStoryboards.length }) }}
- 一键添加全部 + {{ $t('common.addAll') }}
@@ -98,12 +98,12 @@ :icon="Plus" @click.stop="addClipToTimeline(scene)" > - 添加到时间线 + {{ $t('common.addToTimeline') }}
-
镜头 #{{ scene.storyboard_num || scene.assetId }}
+
{{ $t('storyboard.shot') }} #{{ scene.storyboard_num || scene.assetId }}
@@ -116,7 +116,7 @@
- - 重置 + {{ $t('common.reset') }} + {{ Math.round(zoom * 100) }}% @@ -154,7 +154,7 @@ @dragover.prevent @click="clickTimeline($event)" > -
视频轨道
+
{{ $t('video.videoTrack') }}
-
场景{{ clip.storyboard_number }}
+
{{ $t('storyboard.scene') }} {{ clip.storyboard_number }}
{{ clip.duration.toFixed(1) }}s
@@ -204,13 +204,13 @@ @click="clickTimeline($event)" >
- 音频轨道 + {{ $t('video.audioTrack') }} @@ -231,7 +231,7 @@
-
音频 {{ audio.order + 1 }}
+
{{ $t('video.audio') }} {{ audio.order + 1 }}
{{ audio.duration.toFixed(1) }}s
@@ -253,8 +253,8 @@ width="500px" > - - + + @@ -283,7 +283,7 @@ - + { + const stored = localStorage.getItem('language') + if (stored) return stored + + // 自动检测浏览器语言 + const browserLang = navigator.language.toLowerCase() + if (browserLang.startsWith('zh')) return 'zh-CN' + return 'en-US' +} + +const i18n = createI18n({ + legacy: false, // 使用 Composition API 模式 + locale: getStoredLanguage(), + fallbackLocale: 'zh-CN', + messages: { + 'zh-CN': zhCN, + 'en-US': enUS + } +}) + +export default i18n + +// 导出语言切换函数 +export const setLanguage = (lang: string) => { + i18n.global.locale.value = lang as any + localStorage.setItem('language', lang) +} + +export const getCurrentLanguage = () => { + return i18n.global.locale.value +} diff --git a/web/src/locales/zh-CN.ts b/web/src/locales/zh-CN.ts new file mode 100644 index 0000000..23c7d7b --- /dev/null +++ b/web/src/locales/zh-CN.ts @@ -0,0 +1,607 @@ +export default { + nav: { + home: '首页', + characters: '角色管理', + storyboard: '分镜制作', + videos: '视频管理', + assets: '资源库', + settings: '设置', + dramas: '短剧项目' + }, + dashboard: { + title: '🎬 Drama Generator', + welcome: '欢迎使用 AI 短剧生成平台', + subtitle: '从剧本到视频,一站式短剧创作工具', + stats: { + projects: '短剧项目', + images: '生成图片', + videos: '生成视频', + tasks: '处理中任务' + }, + quickStart: '快速开始', + actions: { + newProject: '创建新项目', + newProjectDesc: '开始一个全新的短剧项目', + myProjects: '我的项目', + myProjectsDesc: '查看和管理已有项目' + } + }, + common: { + create: '创建', + edit: '编辑', + delete: '删除', + save: '保存', + cancel: '取消', + confirm: '确定', + search: '搜索', + filter: '筛选', + reset: '重置', + submit: '提交', + close: '关闭', + back: '返回', + next: '下一步', + previous: '上一步', + selectAll: '全选', + loading: '加载中...', + success: '成功', + failed: '失败', + noData: '暂无数据', + pleaseSelect: '请选择', + add: '添加', + view: '查看', + upload: '上传', + download: '下载', + generating: '生成中...', + notGenerated: '未生成', + generateFailed: '生成失败', + clickToRegenerate: '点击重新生成', + queuing: '排队中', + processing: '处理中', + saveAndGenerate: '保存并生成', + saveConfig: '保存配置', + play: '播放', + pause: '暂停', + addAll: '一键添加全部', + addToTimeline: '添加到时间线', + deleteAsset: '删除素材', + confirmDelete: '确认删除', + tip: '提示', + edit: '编辑', + status: '状态', + createdAt: '创建时间', + updatedAt: '更新时间' + }, + settings: { + title: '设置', + aiConfig: 'AI配置', + general: '通用设置', + language: '语言', + theme: '主题' + }, + aiConfig: { + title: 'AI 服务配置', + addConfig: '添加配置', + editConfig: '编辑配置', + back: '返回', + empty: '暂无配置,点击添加配置开始使用', + enabled: '已启用', + disabled: '已禁用', + enable: '启用', + disable: '禁用', + endpoint: '端点', + queryEndpoint: '查询端点', + tabs: { + text: '文本生成', + image: '图片生成', + video: '视频生成' + }, + form: { + name: '配置名称', + namePlaceholder: '例如:OpenAI GPT-4', + provider: '厂商', + providerPlaceholder: '请选择厂商', + providerTip: '选择AI服务提供商', + priority: '优先级', + priorityTip: '数值越大优先级越高,相同模型时优先使用高优先级配置', + model: '模型', + modelPlaceholder: '输入或选择模型名称', + modelTip: '可直接输入模型名称或从列表选择,支持多个模型', + baseUrl: 'Base URL', + baseUrlPlaceholder: 'https://api.openai.com', + baseUrlTip: 'API 服务的基础地址,如 Chatfire: https://api.chatfire.site/v1,Gemini: https://generativelanguage.googleapis.com(无需 /v1)', + fullEndpoint: '完整调用路径', + apiKey: 'API Key', + apiKeyPlaceholder: 'sk-...', + apiKeyTip: '您的 API 密钥', + isActive: '启用状态' + }, + actions: { + test: '测试连接', + delete: '删除', + edit: '编辑' + }, + messages: { + deleteConfirm: '确定要删除此配置吗?', + testSuccess: '连接测试成功!', + testFailed: '连接测试失败' + } + }, + drama: { + title: '短剧管理', + create: '创建项目', + totalProjects: '共 {count} 个项目', + createNew: '创建新项目', + aiConfig: 'AI配置', + aiConfigTip: '请先配置 AI 服务后再创建项目', + empty: '暂无项目,点击上方按钮创建新项目', + status: { + draft: '草稿', + production: '制作中', + completed: '已完成' + }, + actions: { + edit: '编辑', + view: '查看', + delete: '删除' + }, + management: { + overview: '项目概览', + episodes: '章节管理', + characters: '角色管理', + scenes: '场景管理', + projectInfo: '项目信息', + projectName: '项目名称', + projectDesc: '项目描述', + noDescription: '暂无描述', + episodeStats: '章节统计', + characterStats: '角色统计', + sceneStats: '场景统计', + episodesCreated: '已创建章节', + charactersCreated: '已创建角色', + sceneLibraryCount: '场景库数量', + startFirstEpisode: '开始创作您的第一个章节!', + noEpisodesYet: '您的项目还没有章节。请先创建一个章节开始制作。', + createFirstEpisode: '立即创建第一个章节', + episodeList: '章节列表', + createNewEpisode: '创建新章节', + noEpisodes: '还没有章节', + clickToCreate: '点击上方按钮创建第一个章节', + episodeNumber: '第 {number} 章', + goToEdit: '进入编辑', + characterList: '角色列表', + noCharacters: '还没有角色', + charactersTip: '角色将在剧本生成阶段自动创建', + sceneList: '场景列表', + noScenes: '还没有场景', + scenesTip: '场景将在分镜生成阶段自动创建' + } + }, + character: { + title: '角色管理', + create: '创建角色', + edit: '编辑角色', + add: '添加角色', + list: '角色列表', + name: '角色名称', + role: '角色', + personality: '性格', + appearance: '外貌', + background: '背景', + description: '角色描述', + image: '角色形象', + generate: '生成角色形象', + extracting: '提取中...', + generateImage: '生成形象', + batch: '批量操作', + empty: '角色已在剧本生成阶段创建,您可以在此查看和编辑', + backToProject: '返回项目', + saveChanges: '保存修改', + nextStep: '下一步:生成角色图片' + }, + script: { + title: '剧本生成', + backToProject: '返回项目', + aiGenerate: 'AI 生成剧本', + uploadScript: '上传剧本', + steps: { + outline: '生成大纲', + characters: '生成角色', + episodes: '生成剧集' + }, + form: { + theme: '创作主题', + themePlaceholder: '描述你想创作的短剧主题和故事概念', + genre: '类型偏好', + genrePlaceholder: '选择类型', + style: '风格要求', + stylePlaceholder: '例如:轻松幽默、紧张刺激、温馨治愈', + episodeCount: '剧集数量', + randomGenerate: '随机生成', + title: '标题', + titlePlaceholder: '请输入剧本标题', + summary: '概要', + summaryPlaceholder: '请输入剧本概要', + genreExample: '例如:都市、古装', + tags: '标签', + newTag: '新标签' + }, + notice: '请输入创作主题和相关要求,AI将为您生成剧本大纲', + generateFailed: '生成失败', + generating: '生成中...', + nextStep: '下一步', + prevStep: '上一步', + complete: '完成', + regenerate: '重新生成', + regenerateOutline: '重新生成大纲', + outlinePreview: '大纲预览(可编辑)' + }, + imageDialog: { + title: 'AI 图片生成', + selectDrama: '选择剧本', + selectScene: '选择场景', + selectSceneOptional: '选择场景(可选)', + sceneLabel: '场景{number}: {title}', + prompt: '提示词', + promptPlaceholder: '描述你想生成的图片\n例如:A beautiful landscape with mountains and rivers at sunset, cinematic lighting, highly detailed', + negativePrompt: '反向提示词', + negativePromptPlaceholder: '描述不希望出现的元素(可选)\n例如:blurry, low quality, watermark', + aiService: 'AI 服务', + selectService: '选择服务', + imageSize: '图片尺寸', + selectSize: '选择尺寸', + square: '正方形', + landscape: '横向', + portrait: '纵向', + imageQuality: '图片质量', + standard: '标准', + hd: '高清', + style: '风格', + vivid: '鲜艳', + natural: '自然', + advancedSettings: '高级设置', + samplingSteps: '采样步数', + promptRelevance: '提示词相关性', + randomSeed: '随机种子', + leaveBlankRandom: '留空随机', + seedTip: '设置相同种子可复现图片', + generate: '生成图片', + pleaseSelectDrama: '请选择剧本', + pleaseEnterPrompt: '请输入提示词', + promptMinLength: '提示词至少5个字符', + taskSubmitted: '图片生成任务已提交,请稍后查看结果', + generateFailed: '生成失败', + weak: '弱', + moderate: '适中', + strong: '强', + veryStrong: '很强' + }, + image: { + title: 'AI 图片生成', + generate: '生成图片', + loadFailed: '加载失败', + generating: '生成中...', + generateFailed: '生成失败' + }, + dramaWorkflow: { + returnToList: '返回', + episodeScript: '第{number}集剧本', + storyboardBreakdown: '分镜拆解', + characterImages: '角色图片', + createChapterPrompt: '请创建第一章开始制作', + createChapter: '创建第{number}章', + nextStepCharacterImages: '下一步:角色图片', + nextStep: '下一步', + reGenerateShots: '重新拆分', + reGenerateShotsConfirm: '重新拂分将覆盖现有镜头,确定继续吗?', + pleaseWriteScript: '请先创作剧本内容', + splitStoryboardFirst: '请先对剧本进行分镜拆解', + aiSplitting: 'AI拆分中...', + aiAutoSplit: 'AI自动拆分', + selected: '已选', + characterCount: '角色数', + generated: '已生成', + batchGenerate: '批量生成' + }, + workflow: { + backToProject: '返回项目', + episodeProduction: '第{number}章制作', + steps: { + content: '章节内容', + generateImages: '生成图片', + splitStoryboard: '拆分分镜' + }, + scriptPlaceholder: '请输入章节内容...', + saveChapter: '保存章节', + chapterContent: '第{number}章内容', + saved: '已保存', + extractedData: '已提取数据', + characters: '角色', + scenes: '场景', + extractedCharacters: '提取的角色(本集)', + extractedScenes: '提取的场景(本集)', + extractCharactersAndScenes: '提取角色和场景', + reExtract: '重新提取角色和场景', + nextStepGenerateImages: '下一步:生成图片', + extractWarning: '请先点击“提取角色和场景”按钮,完成提取后才能生成图片', + characterImages: '角色图片', + sceneImages: '场景图片', + characterCount: '共 {count} 个角色需要生成图片', + sceneCount: '共 {count} 个场景需要生成图片', + selectAll: '全选', + batchGenerate: '批量生成', + modelConfig: 'AI模型配置', + editPrompt: '修改提示词', + aiGenerate: 'AI生成', + uploadImage: '上传图片', + selectFromLibrary: '从角色库选择', + shotList: '镜头列表', + dragFilesHere: '将文件拖到此处,或', + clickToUpload: '点击上传', + prevStep: '上一步', + nextStepSplitShots: '下一步:拆分分镜', + reExtractConfirmTitle: '重新提取确认', + reExtractConfirmMessage: '重新提取将覆盖已提取的角色和场景(包括已生成的图片),确定继续吗?', + startReExtracting: '开始重新提取,请稍候...', + regenerateShots: '重新生成分镜', + batchGenerateSelected: '批量生成选中场景', + generateAllImagesFirst: '请先生成所有角色和场景图片后再进行分镜拆分', + sceneImageGenerating: '场景图片生成中,请稍候...', + sceneImageComplete: '场景图片生成完成!', + sceneImageStarted: '场景图片生成已启动', + reSplitShots: '重新拆分', + enterProfessional: '进入专业制作', + editShot: '编辑镜头', + splitSuccess: '分镜拆分成功!正在进入专业制作界面...', + reSplitConfirm: '确定要重新拂分分镜吗?', + deleteCharacter: '删除角色', + splitStoryboardFirst: '请先对章节进行分镜拆解', + aiSplitting: 'AI拆分中...', + aiAutoSplit: 'AI自动拆分', + batchTaskSubmitted: '批量生成任务已提交!', + batchGenerateFailed: '批量生成失败', + batchCompleteSuccess: '批量生成完成!成功生成 {count} 个场景', + batchCompletePartial: '生成完成:成功 {success} 个,失败 {fail} 个', + addToLibrary: '添加到角色库', + addToLibraryConfirm: '确定要将角色“{name}”添加到全局角色库吗?添加后可以在所有项目中使用该角色形象。', + addedToLibrary: '已添加到角色库!', + addFailed: '添加失败', + shotTitle: '镜头标题', + shotTitlePlaceholder: '请输入镜头标题', + shotType: '景别', + selectShotType: '选择景别', + longShot: '远景', + fullShot: '全景', + mediumShot: '中景', + closeUp: '近景', + extremeCloseUp: '特写', + cameraAngle: '镜头角度', + selectAngle: '选择角度', + eyeLevel: '平视', + lowAngle: '仰视', + highAngle: '俯视', + location: '地点', + locationPlaceholder: '场景地点', + shotDescription: '镜头描述', + shotDescriptionPlaceholder: '镜头整体描述', + cameraMovement: '运镜方式', + selectMovement: '选择运镜', + staticShot: '固定镜头', + pushIn: '推镜', + pullOut: '拉镜', + followShot: '跟镜', + sideView: '侧面', + time: '时间', + timeSetting: '时间设定', + actionDescription: '动作描述', + detailedAction: '详细动作描述', + dialogue: '对白', + characterDialogue: '角色对白', + generateImageFirst: '请先生成角色图片', + result: '画面结果', + actionResult: '动作结果', + atmosphere: '环境氛围', + atmosphereDescription: '环境氛围描述', + loadLibraryFailed: '获取角色库失败', + imagePrompt: '图片提示词', + imagePromptPlaceholder: '用于AI生成图片的提示词', + videoPrompt: '视频提示词', + videoPromptPlaceholder: '用于AI生成视频的提示词', + bgmHint: '配乐提示', + bgmAtmosphere: '配乐氛围描述', + soundEffect: '音效', + soundEffectDescription: '音效描述', + durationSeconds: '时长(秒)', + emptyLibrary: '角色库为空,请先生成或上传角色图片', + textModelTip: '用于生成章节内容、角色、场景等文本', + uploadFormatTip: '支持 jpg/png 格式,文件大小不超过 10MB', + aiModelConfig: 'AI模型配置', + textGenModel: '文本生成模型', + imageGenModel: '图片生成模型', + selectTextModel: '选择文本生成模型', + selectImageModel: '选择图片生成模型', + modelConfigTip: '用于生成角色和场景图片', + modelConfigSaved: '模型配置已保存', + pleaseSelectModels: '请选择文本和图片生成模型' + }, + professionalEditor: { + duration: '时长', + seconds: '秒', + videoDuration: '视频时长', + downloadVideo: '下载视频' + }, + storyboard: { + title: '分镜制作', + edit: '分镜编辑', + create: '创建分镜', + script: '剧本', + scene: '场景', + shot: '镜头', + shotNumber: '镜头 {number}', + untitled: '未命名镜头', + scriptStructure: '剧本结构', + add: '添加', + noStoryboard: '暂无分镜', + shotProperties: '镜头属性', + selectScene: '选择场景', + inDevelopment: '功能开发中...', + generateScript: '生成分镜脚本', + generateImage: '生成分镜图片', + generateVideo: '生成视频', + table: { + number: '编号', + title: '标题', + shotType: '景别', + movement: '运镜', + location: '地点', + character: '角色', + dialogue: '对白', + action: '动作', + duration: '时长', + operations: '操作' + } + }, + timeline: { + title: '时间线编辑器', + backToEditor: '返回', + noScenes: '暂无可用场景', + loadFailed: '加载分镜失败' + }, + editor: { + backToEpisode: '返回剧集编辑', + episode: '第{number}集', + settings: '设置', + basicInfo: '基础信息', + sceneProduction: '场景制作', + sceneId: '场景ID', + sceneGenerating: '场景图片生成中...', + noBackground: '未关联背景', + cast: '登场角色', + addCharacter: '添加角色', + removeCharacter: '移除角色', + noCharacters: '未指定角色', + visualSettings: '视效设置', + shotType: '景别', + shotTypePlaceholder: '选择景别', + movement: '运镜方式', + movementPlaceholder: '运镜方式', + angle: '镜头角度', + anglePlaceholder: '镜头角度', + action: '动作描述', + actionPlaceholder: '描述角色的动作过程...', + result: '动作结果', + resultPlaceholder: '描述动作的结果...', + dialogue: '对白', + dialoguePlaceholder: '输入角色对白...', + soundEffects: '音效', + soundEffectsPlaceholder: '描述音效...', + transitions: '转场效果', + transitionsPlaceholder: '选择转场', + duration: '时长', + seconds: '秒', + description: '镜头描述', + descriptionPlaceholder: '整体镜头描述...', + bgmPrompt: '配乐提示', + bgmPromptPlaceholder: '描述配乐氛围,如:紧张激烈的背景音乐', + atmosphere: '环境氛围', + atmospherePlaceholder: '描述环境氛围,如:昱暗压抑、明亮温馨', + lightingEffect: '光照效果', + specialEffects: '特效', + props: '道具', + emotionalTone: '情绪色调', + shotImage: '镜头图片', + noShotSelected: '未选择镜头', + selectFrameType: '选择帧类型', + firstFrame: '首帧', + lastFrame: '尾帧', + panelFrame: '分镜板', + actionSequence: '动作序列', + keyFrame: '关键帧', + panelCount: '格数', + prompt: '提示词', + extractPrompt: '提取提示词', + promptPlaceholder: '点击提取提示词按钮,系统将根据分镜内容生成图片提示词...', + generating: '生成中...', + generateImage: '生成图片', + uploadImage: '上传图片', + generationResult: '生成结果' + }, + video: { + title: 'AI 视频生成', + generate: '生成视频', + merge: '合成视频', + mediaLibrary: '视频素材库', + videoCount: '{count} 个视频', + dragToTimeline: '将场景拖拽到时间线开始编辑', + videoTrack: '视频轨道', + audioTrack: '音频轨道', + soundAndMusic: '音效与配乐', + soundMusicInDev: '音效与配乐功能开发中', + noMergeYet: '还没有合成过视频', + mergeInstructions: '在时间线编辑器中排列好视频后点击“合成视频”即可', + selectVideoModel: '请选择视频模型', + mergeComplete: '视频合成完成并已下载!', + mergeTaskSubmitted: '视频合成任务已提交,正在后台处理...', + audio: '音频', + extractAudio: '从所有视频片段提取音频', + model: '模型', + videoGeneration: '视频生成', + soundAndMusicTab: '音效与配乐', + videoMerge: '视频合成', + noMergeRecords: '暂无视频合成记录', + transitionType: '转场类型', + transitionDuration: '转场时长', + selectTransition: '选择转场效果', + filter: { + drama: '剧本', + allDramas: '全部剧本', + status: '状态', + allStatus: '全部状态', + query: '查询', + reset: '重置' + }, + status: { + pending: '等待中', + processing: '生成中', + completed: '已完成', + failed: '失败' + }, + prompt: '提示词', + duration: '时长', + createdAt: '创建时间', + actions: { + view: '查看详情', + download: '下载', + delete: '删除' + } + }, + asset: { + title: '资源库', + type: '资源类型', + upload: '上传', + import: '导入', + export: '导出' + }, + genres: { + urban: '都市', + costume: '古装', + mystery: '悬疑', + romance: '爱情', + comedy: '喜剧' + }, + tooltip: { + editPrompt: '修改提示词', + aiGenerate: 'AI生成', + uploadImage: '上传图片', + selectFromLibrary: '从角色库选择' + }, + message: { + deleteConfirm: '确定要删除吗?', + deleteSuccess: '删除成功', + createSuccess: '创建成功', + updateSuccess: '更新成功', + operationSuccess: '操作成功', + operationFailed: '操作失败', + loadingFailed: '加载失败', + networkError: '网络错误' + } +} diff --git a/web/src/main.ts b/web/src/main.ts index f99aa92..6c24eb0 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -6,12 +6,14 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue' import App from './App.vue' import router from './router' +import i18n from './locales' import './assets/styles/main.css' const app = createApp(App) app.use(createPinia()) app.use(router) +app.use(i18n) app.use(ElementPlus) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { diff --git a/web/src/views/dashboard/Dashboard.vue b/web/src/views/dashboard/Dashboard.vue index a052560..e29a362 100644 --- a/web/src/views/dashboard/Dashboard.vue +++ b/web/src/views/dashboard/Dashboard.vue @@ -3,14 +3,15 @@
-

🎬 Drama Generator

+

{{ $t('dashboard.title') }}

+
-

欢迎使用 AI 短剧生成平台

-

从剧本到视频,一站式短剧创作工具

+

{{ $t('dashboard.welcome') }}

+

{{ $t('dashboard.subtitle') }}

@@ -19,7 +20,7 @@

0

-

短剧项目

+

{{ $t('dashboard.stats.projects') }}

@@ -29,7 +30,7 @@

0

-

生成图片

+

{{ $t('dashboard.stats.images') }}

@@ -39,7 +40,7 @@

0

-

生成视频

+

{{ $t('dashboard.stats.videos') }}

@@ -49,28 +50,28 @@

0

-

处理中任务

+

{{ $t('dashboard.stats.tasks') }}

-

快速开始

+

{{ $t('dashboard.quickStart') }}

-

创建新项目

-

开始一个全新的短剧项目

+

{{ $t('dashboard.actions.newProject') }}

+

{{ $t('dashboard.actions.newProjectDesc') }}

-

我的项目

-

查看和管理已有项目

+

{{ $t('dashboard.actions.myProjects') }}

+

{{ $t('dashboard.actions.myProjectsDesc') }}

@@ -84,6 +85,7 @@