diff --git a/.env b/.env index 5f3078a..9c174f2 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ VITE_ARK_API_KEY=4be01684-0d43-46c0-9bc9-533213afc982 VITE_BASE_URL=/scriptflow/ +VITE_KIE_API_KEY=35863f600b3306c1225c54f8f60bf5d4 \ No newline at end of file diff --git a/.env.example b/.env.example index 8dc5ad0..6d64f19 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# GEMINI_API_KEY: Required for Gemini AI API calls. +# GEMINI_API_KEY: Required for Gemini AI API calls. # AI Studio automatically injects this at runtime from user secrets. # Users configure this via the Secrets panel in the AI Studio UI. GEMINI_API_KEY="MY_GEMINI_API_KEY" @@ -14,4 +14,11 @@ VITE_ARK_API_KEY= # VITE_BASE_URL: Optional base path for frontend assets and routes. # Example: "/" for root deployment, "/scriptflow/" for subpath deployment. -VITE_BASE_URL=/ \ No newline at end of file +VITE_BASE_URL=/ +# VITE_KIE_API_KEY: Kie AI token for nano-banana-2 image generation. +VITE_KIE_API_KEY= + +# VITE_KIE_CALLBACK_URL: Optional public callback URL for Kie async task webhooks. +# Must be publicly reachable by Kie servers. +VITE_KIE_CALLBACK_URL= + diff --git a/src/App.tsx b/src/App.tsx index bf761c5..30f316f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Sparkles, @@ -38,6 +38,7 @@ import { refineConvertedScript, generatePageScript, generateAllScripts, + generatePosterCompositionSuggestion, extractEpisodesFromSource, extractPlotBackgroundFromSource, ScriptOption, @@ -50,20 +51,127 @@ import { normalizeSceneHeadingMarkers, splitScriptPreviewBlocks } from './lib/sc import { parseSavedConversionEpisodeResults, sanitizeSavedConversionEpisodeResults } from './lib/conversionDraft'; import { parseFinalizedOutline } from './lib/finalizedOutline'; import { getDisplayedModeLabel } from './lib/modeSource'; +import { createNanoBanana2Task, getKieTaskDetail } from './services/kieImage'; type ScriptType = '\u77ed\u5267' | '\u7535\u89c6\u5267' | '\u7535\u5f71'; -type ScriptFormat = '全部' | '场景' | '人物' | '台词' | '动作' | '镜头提示'; -type ThemeLabel = '悬疑' | '侦探' | '犯罪' | '反转' | '惊悚' | '推理'; +type ScriptFormat = '\u5b8c\u6574\u5267\u672c' | '\u573a\u666f' | '\u4eba\u7269' | '\u5bf9\u767d' | '\u52a8\u4f5c' | '\u955c\u5934\u8bed\u8a00'; +type ThemeLabel = '\u60ac\u7591' | '\u4fa6\u63a2' | '\u72af\u7f6a' | '\u53cd\u8f6c' | '\u60ca\u609a' | '\u63a8\u7406'; type NarrativeMethod = '\u987a\u53d9' | '\u5012\u53d9' | '\u63d2\u53d9' | '\u7b2c\u4e00\u4eba\u79f0' | '\u7b2c\u4e09\u4eba\u79f0\u5168\u77e5 / \u9650\u77e5' | '\u591a\u7ebf\u53d9\u4e8b' | '\u73af\u5f62\u53d9\u4e8b' | '\u8499\u592a\u5947' | '\u7559\u767d\u53d9\u4e8b' | '\u60ac\u5ff5\u53d9\u4e8b'; type AudiencePreference = '\u7537\u9891' | '\u5973\u9891'; type WordRange = '200 - 500' | '500 - 1000' | '1000 - 2000' | '2000 - 3000' | '3000\u4ee5\u4e0a' | '\u4e0d\u9650'; +type AssetCenterTab = 'portraits' | 'relations' | 'poster'; +type AssetResultBuckets = Record; +type AssetTextBuckets = Record; +type AssetSelectionBuckets = Record; +type AssetVisualStyle = 'photorealistic' | 'game3d' | 'anime2d' | 'pixar'; +type AssetGenerationStatus = 'idle' | 'generating' | 'success' | 'error'; +type AssetGenerationStatusBuckets = Record; +type AssetGenerationMessageBuckets = Record; +type PosterPromptFieldKey = 'title' | 'hook' | 'visual' | 'cast' | 'ratio' | 'composition' | 'characterStyle'; +type PosterPromptFields = Record; +const POSTER_CHARACTER_STYLE_OPTIONS = [ + '\u771f\u4eba\u5199\u5b9e', + '3D CG \u6e38\u620f\u98ce\u683c', + '2D \u52a8\u6f2b', + '\u8fea\u58eb\u5c3c\u76ae\u514b\u65af\u98ce\u683c', +] as const; + +function getPosterCharacterStylePrompt(style: string) { + const normalized = (style || '').trim(); + if (normalized === '3D CG \u6e38\u620f\u98ce\u683c') { + return '\u4eba\u7269\u89d2\u8272\u98ce\u683c\uff1a3D CG \u6e38\u620f\u7f8e\u672f\uff0c\u786c\u8868\u9762\u548c\u5e03\u6599\u7ec6\u8282\u660e\u786e\uff0c\u89d2\u8272\u9020\u578b\u5199\u5b9e\u4f46\u5e26\u98ce\u683c\u5316\u8d28\u611f\u3002'; + } + if (normalized === '2D \u52a8\u6f2b') { + return '\u4eba\u7269\u89d2\u8272\u98ce\u683c\uff1a2D \u52a8\u6f2b\uff0c\u7ebf\u7a3f\u5e72\u51c0\uff0c\u8272\u5757\u6e05\u6670\uff0c\u89d2\u8272\u5f62\u8c61\u4e8c\u7ef4\u5316\u8bbe\u8ba1\u611f\u5f3a\u3002'; + } + if (normalized === '\u8fea\u58eb\u5c3c\u76ae\u514b\u65af\u98ce\u683c') { + return '\u4eba\u7269\u89d2\u8272\u98ce\u683c\uff1a\u8fea\u58eb\u5c3c\u76ae\u514b\u65af\u5411\uff0c\u5706\u6da6\u8f6e\u5ed3\uff0c\u52a8\u753b\u7535\u5f71\u611f\u6750\u8d28\uff0c\u8868\u60c5\u53ef\u8bfb\u6027\u5f3a\u3002'; + } + return '\u4eba\u7269\u89d2\u8272\u98ce\u683c\uff1a\u771f\u4eba\u5199\u5b9e\uff0c\u4eba\u7269\u6bd4\u4f8b\u81ea\u7136\uff0c\u76ae\u80a4\u4e0e\u670d\u9970\u7eb9\u7406\u771f\u5b9e\uff0c\u5149\u5f71\u8d34\u5408\u5199\u5b9e\u6444\u5f71\u3002'; +} const SCRIPT_TYPES: ScriptType[] = ['\u77ed\u5267', '\u7535\u89c6\u5267', '\u7535\u5f71']; const THEME_OPTIONS: ThemeLabel[] = ['\u60ac\u7591', '\u4fa6\u63a2', '\u72af\u7f6a', '\u53cd\u8f6c', '\u60ca\u609a', '\u63a8\u7406']; const NARRATIVE_OPTIONS: NarrativeMethod[] = ['\u987a\u53d9', '\u5012\u53d9', '\u63d2\u53d9', '\u7b2c\u4e00\u4eba\u79f0', '\u7b2c\u4e09\u4eba\u79f0\u5168\u77e5 / \u9650\u77e5', '\u591a\u7ebf\u53d9\u4e8b', '\u73af\u5f62\u53d9\u4e8b', '\u8499\u592a\u5947', '\u7559\u767d\u53d9\u4e8b', '\u60ac\u5ff5\u53d9\u4e8b']; const AUDIENCE_OPTIONS: AudiencePreference[] = ['\u7537\u9891', '\u5973\u9891']; const WORD_RANGE_OPTIONS: WordRange[] = ['200 - 500', '500 - 1000', '1000 - 2000', '2000 - 3000', '3000\u4ee5\u4e0a', '\u4e0d\u9650']; - +const KIE_POLL_INTERVAL_MS = 5000; +const KIE_POLL_MAX_ATTEMPTS = 60; +const RELATION_REFERENCE_MAX = 14; +const ASSET_STYLE_CONFIG: Record = { + photorealistic: { + label: '\u771f\u4eba\u5199\u5b9e', + prompt: '\u98ce\u683c\u7ea6\u675f\uff1a\u771f\u4eba\u5199\u5b9e\uff0cphotorealistic\uff0c\u771f\u5b9e\u76ae\u80a4\u7eb9\u7406\u4e0e\u6bdb\u53d1\u7ec6\u8282\uff0c\u81ea\u7136\u5149\u5f71\u4e0e\u955c\u5934\u666f\u6df1\uff0c\u6bd4\u4f8b\u771f\u5b9e\uff0c\u7ec6\u8282\u6e05\u6670\u3002', + negative: '\u4e0d\u8981\u5361\u901a\u611f\uff0c\u4e0d\u8981\u63d2\u753b\u611f\uff0c\u4e0d\u8981\u52a8\u6f2b\u7ebf\u7a3f\uff0c\u4e0d\u89813D\u5851\u6599\u611f\uff0c\u4e0d\u8981\u8fc7\u5ea6\u78e8\u76ae\uff0c\u4e0d\u8981\u5938\u5f20\u6bd4\u4f8b\uff0c\u4e0d\u8981\u6587\u5b57\u6c34\u5370\u3002' + }, + game3d: { + label: '3D CG \u6e38\u620f\u98ce\u683c', + prompt: '\u98ce\u683c\u7ea6\u675f\uff1a3D CG game art\uff0c\u6b21\u4e16\u4ee3\u6e38\u620f\u89d2\u8272\u8bbe\u5b9a\u56fe\uff0cPBR\u6750\u8d28\uff0c\u786c\u8868\u9762\u4e0e\u5e03\u6599\u7ec6\u8282\u6e05\u6670\uff0c\u4f53\u79ef\u5149\u548c\u8f6e\u5ed3\u5149\uff0c\u8d34\u56fe\u8d28\u611f\u771f\u5b9e\u3002', + negative: '\u4e0d\u8981\u771f\u4eba\u6444\u5f71\u611f\uff0c\u4e0d\u89812D\u5e73\u9762\u63d2\u753b\uff0c\u4e0d\u8981\u52a8\u6f2b\u7ebf\u7a3f\uff0c\u4e0d\u8981\u8fea\u58eb\u5c3c\u5361\u901a\u8138\uff0c\u4e0d\u8981\u4f4e\u6a21\u7ce0\u8d34\u56fe\uff0c\u4e0d\u8981\u6587\u5b57\u6c34\u5370\u3002' + }, + anime2d: { + label: '2D \u52a8\u6f2b', + prompt: '\u98ce\u683c\u7ea6\u675f\uff1a2D anime character design\uff0c\u65e5\u7cfb\u52a8\u6f2b\u98ce\uff0c\u7ebf\u7a3f\u6e05\u6670\uff0c\u8d5b\u7490\u7490\u5e73\u6d82\uff0c\u5e72\u51c0\u5206\u533a\u9634\u5f71\uff0c\u4e8c\u7ef4\u5e73\u9762\u89d2\u8272\u8bbe\u5b9a\u56fe\u3002', + negative: '\u4e0d\u8981\u7167\u7247\u5199\u5b9e\uff0c\u4e0d\u89813D\u6e32\u67d3\uff0c\u4e0d\u8981\u5199\u5b9e\u6750\u8d28\u9ad8\u5149\uff0c\u4e0d\u8981\u8fea\u58eb\u5c3c\u9762\u90e8\u98ce\u683c\uff0c\u4e0d\u8981\u6587\u5b57\u6c34\u5370\u3002' + }, + pixar: { + label: '\u8fea\u58eb\u5c3c\u76ae\u514b\u65af\u98ce\u683c', + prompt: '\u98ce\u683c\u7ea6\u675f\uff1aDisney Pixar inspired style\uff0c\u98ce\u683c\u53163D\u89d2\u8272\uff0c\u5706\u6da6\u9020\u578b\uff0c\u6e29\u6696\u7535\u5f71\u5149\u6548\uff0c\u52a8\u753b\u7535\u5f71\u7ea7\u6e32\u67d3\u8d28\u611f\u3002', + negative: '\u4e0d\u8981\u771f\u4eba\u6444\u5f71\u8d28\u611f\uff0c\u4e0d\u8981\u65e5\u6f2b\u7ebf\u7a3f\uff0c\u4e0d\u8981\u786c\u6838\u5199\u5b9e\u6e38\u620f\u6750\u8d28\uff0c\u4e0d\u8981\u6050\u6016\u9634\u6697\u98ce\uff0c\u4e0d\u8981\u6587\u5b57\u6c34\u5370\u3002' + } +}; +const POSTER_PROMPT_FIELD_META: Array<{ + key: PosterPromptFieldKey; + label: string; + hint: string; + multiline?: boolean; + placeholder: string; +}> = [ + { + key: 'title', + label: '\u6d77\u62a5\u6807\u9898', + hint: '\u5efa\u8bae 4-10 \u5b57\uff0c\u53ef\u7528\u4e3b\u6807+\u526f\u6807\u5f62\u5f0f', + placeholder: '\u4f8b\uff1a\u6d88\u5931\u7684\u8bc1\u4eba' + }, + { + key: 'hook', + label: '\u4e00\u53e5\u8bdd\u5356\u70b9', + hint: '\u7528\u4e00\u53e5\u6709\u5f20\u529b\u7684\u6587\u6848\u6982\u62ec\u51b2\u7a81', + placeholder: '\u4f8b\uff1a\u4e00\u901a\u6765\u81ea\u5341\u5e74\u524d\u7684\u7535\u8bdd\uff0c\u628a\u771f\u76f8\u63a8\u5411\u4e86\u98ce\u66b4\u4e2d\u5fc3' + }, + { + key: 'visual', + label: '\u89c6\u89c9\u5173\u952e\u8bcd', + hint: '\u5efa\u8bae 4-8 \u4e2a\u8bcd\uff0c\u7528\u3001\u5206\u9694', + placeholder: '\u4f8b\uff1a\u96e8\u591c\u3001\u9713\u8679\u9006\u5149\u3001\u7d27\u5f20\u5bf9\u5cd9\u3001\u51b7\u6696\u5bf9\u6bd4' + }, + { + key: 'cast', + label: '\u4e3b\u89d2\u51fa\u955c', + hint: '\u8bf4\u660e\u4e3b\u6b21\u89d2\u7684\u7ad9\u4f4d\u4e0e\u5c42\u6b21', + multiline: true, + placeholder: '\u4f8b\uff1a\u524d\u666f\u6797\u9ed8\u624b\u6301\u8bc1\u7269\uff0c\u4e2d\u666f\u5468\u542f\u51b7\u51dd\u6ce8\u89c6\uff0c\u80cc\u666f\u8b66\u706f\u62c9\u51fa\u538b\u8feb\u611f' + }, + { + key: 'ratio', + label: '\u753b\u5e45\u6bd4\u4f8b', + hint: '\u9009\u62e9\u6a2a\u5c4f/\u7ad6\u5c4f\uff0c\u5c06\u5f71\u54cd\u6784\u56fe\u4e0e\u5149\u5f71\u5206\u6790', + placeholder: '16:9' + }, + { + key: 'composition', + label: '\u6784\u56fe\u4e0e\u5149\u5f71', + hint: '\u660e\u786e\u89c6\u89c9\u4e2d\u5fc3\uff0c\u7a7a\u95f4\u5c42\u6b21\u548c\u706f\u5149\u65b9\u5411', + multiline: true, + placeholder: '\u4f8b\uff1a\u4e09\u89d2\u6784\u56fe\uff0c\u4e3b\u89d2\u5c45\u4e2d\u7565\u504f\u5de6\uff0c\u6e7f\u5730\u5012\u5f71\u589e\u5f3a\u6df1\u5ea6\uff0c\u9ad8\u5bf9\u6bd4\u4fa7\u9006\u5149' + }, + { + key: 'characterStyle', + label: '\u4eba\u7269\u89d2\u8272\u98ce\u683c', + hint: '\u63a7\u5236\u6d77\u62a5\u91cc\u4eba\u7269\u7684\u89c6\u89c9\u98ce\u683c\u4e0e\u8d28\u611f', + placeholder: '\u771f\u4eba\u5199\u5b9e' + } +]; interface CreationPage { id: string; story: string; @@ -102,6 +210,94 @@ function countWords(value: string) { return value.replace(/\s+/g, '').length; } +function getAssetBucketKey(tab: AssetCenterTab, selectedCardIds: string[]) { + const sortedIds = [...selectedCardIds].sort(); + const roleScope = sortedIds.length > 0 ? sortedIds.join('|') : '__none__'; + return `${tab}::${roleScope}`; +} + +function parseSavedAssetBuckets(saved: string | null): AssetResultBuckets { + if (!saved) return {}; + try { + const parsed = JSON.parse(saved) as unknown; + if (Array.isArray(parsed)) { + const legacyUrls = parsed.filter((item): item is string => typeof item === 'string'); + return legacyUrls.length > 0 ? { [getAssetBucketKey('portraits', [])]: legacyUrls } : {}; + } + if (parsed && typeof parsed === 'object') { + return parsed as AssetResultBuckets; + } + } catch { + return {}; + } + return {}; +} + +function parseSavedAssetTextBuckets(saved: string | null): AssetTextBuckets { + if (!saved) return {}; + try { + const parsed = JSON.parse(saved) as unknown; + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as AssetTextBuckets; + } + } catch { + return {}; + } + return {}; +} + +function buildPosterPromptDraftFromFields(fields: PosterPromptFields) { + return POSTER_PROMPT_FIELD_META + .map((meta) => `${meta.label}\uFF1A${(fields[meta.key] || '').trim()}`) + .join('\n'); +} + +function parsePosterPromptFieldsFromDraft(draft: string, fallback: PosterPromptFields): PosterPromptFields { + const next: PosterPromptFields = { ...fallback }; + const raw = (draft || '').trim(); + if (!raw) return next; + + const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + for (const line of lines) { + for (const meta of POSTER_PROMPT_FIELD_META) { + if (line.startsWith(`${meta.label}\uFF1A`) || line.startsWith(`${meta.label}:`)) { + next[meta.key] = line.replace(`${meta.label}\uFF1A`, '').replace(`${meta.label}:`, '').trim(); + } + } + } + + return next; +} + +function posterPromptTextFromFields(fields: PosterPromptFields) { + return POSTER_PROMPT_FIELD_META + .map((meta) => `${meta.label}\uFF1A${(fields[meta.key] || '').trim() || '\u5f85\u8865\u5145'}`) + .join('\n'); +} + +function parseSavedAssetSelectionBuckets(saved: string | null): AssetSelectionBuckets { + const empty: AssetSelectionBuckets = { portraits: [], relations: [], poster: [] }; + if (!saved) return empty; + try { + const parsed = JSON.parse(saved) as unknown; + if (Array.isArray(parsed)) { + const legacyIds = parsed.filter((item): item is string => typeof item === 'string'); + return { ...empty, portraits: legacyIds }; + } + if (parsed && typeof parsed === 'object') { + const value = parsed as Partial>; + return { + portraits: Array.isArray(value.portraits) ? value.portraits.filter((item): item is string => typeof item === 'string') : [], + relations: Array.isArray(value.relations) ? value.relations.filter((item): item is string => typeof item === 'string') : [], + poster: Array.isArray(value.poster) ? value.poster.filter((item): item is string => typeof item === 'string') : [], + }; + } + } catch { + return empty; + } + return empty; +} + function parseEpisodesFromText(text: string): ExtractedEpisode[] { const normalized = text.replace(/\r\n/g, '\n').trim(); if (!normalized) return []; @@ -224,6 +420,13 @@ interface CharacterPreviewCard { rawContent: string; } +const PORTRAIT_FIELD_LABELS = new Set([ + '\u5916\u8c8c', + '\u7279\u70b9', + '\u8eab\u4efd', + '\u6027\u683c' +]); + const CHARACTER_PREVIEW_FIELD_LABELS = new Set([ '\u8eab\u4efd', '\u6027\u683c', @@ -238,6 +441,7 @@ const CHARACTER_PREVIEW_FIELD_LABELS = new Set([ '\u89d2\u8272\u540d', '\u4ecb\u7ecd' ]); +const REQUIRED_CHARACTER_CARD_FIELDS = ['\u8eab\u4efd', '\u6027\u683c', '\u5916\u8c8c', '\u5173\u7cfb'] as const; function parseCharacterProfileCards(text: string): CharacterPreviewCard[] { const normalized = text.replace(/\r\n/g, '\n').trim(); @@ -259,8 +463,7 @@ function parseCharacterProfileCards(text: string): CharacterPreviewCard[] { const line = contentLines[i]; const match = line.match(/^([^\uff1a:]{1,12})[\uff1a:]\s*(.*)$/); if (match && CHARACTER_PREVIEW_FIELD_LABELS.has(match[1].trim())) { - const rawLabel = match[1].trim(); - const label = rawLabel === '\u52a8\u673a' ? '\u5916\u8c8c' : rawLabel; + const label = match[1].trim(); let value = match[2].trim(); if (!value && i + 1 < contentLines.length) { @@ -304,6 +507,51 @@ function parseCharacterProfileCards(text: string): CharacterPreviewCard[] { }).filter((card) => card.fields.length > 0 || card.rawContent || card.title); } +function serializeCharacterProfileCards(cards: CharacterPreviewCard[]): string { + return cards + .map((card) => { + const lines: string[] = []; + const title = (card.title || '').trim(); + if (title) lines.push(title); + card.fields.forEach((field) => { + const label = (field.label || '').trim(); + const value = (field.value || '').trim(); + if (label) lines.push(`${label}\uFF1A${value || '\u5f85\u8865\u5145'}`); + }); + const rawContent = (card.rawContent || '').trim(); + if (rawContent) lines.push(rawContent); + return lines.join('\n').trim(); + }) + .filter(Boolean) + .join('\n\n'); +} + +function getCardFieldValue(card: CharacterPreviewCard, label: string) { + const found = card.fields.find((field) => field.label === label); + return (found?.value || '').trim(); +} + +function upsertCardField(card: CharacterPreviewCard, label: string, value: string): CharacterPreviewCard { + const existingIndex = card.fields.findIndex((field) => field.label === label); + if (existingIndex === -1) { + return { + ...card, + fields: [...card.fields, { label, value }] + }; + } + return { + ...card, + fields: card.fields.map((field, index) => (index === existingIndex ? { ...field, value } : field)) + }; +} +function createEmptyCharacterCard(index: number): CharacterPreviewCard { + return { + id: `character-${Date.now()}-${index}`, + title: `\u89d2\u8272 ${index + 1}`, + fields: REQUIRED_CHARACTER_CARD_FIELDS.map((label) => ({ label, value: '' })), + rawContent: '', + }; +} export default function App() { const [activeTab, setActiveTab] = useState<'conversion' | 'creation' | 'finalized'>('conversion'); @@ -325,14 +573,14 @@ export default function App() { // Common States (Three-level Filters) const [scriptType, setScriptType] = useState('\u77ed\u5267'); - const [scriptFormat, setScriptFormat] = useState('全部'); - const [selectedThemes, setSelectedThemes] = useState(['悬疑']); + const [scriptFormat, setScriptFormat] = useState('\u5b8c\u6574\u5267\u672c'); + const [selectedThemes, setSelectedThemes] = useState(['\u60ac\u7591']); const [selectedNarratives, setSelectedNarratives] = useState(['\u987a\u53d9']); const [audiencePreference, setAudiencePreference] = useState(() => (localStorage.getItem('scriptflow_audience_preference') as AudiencePreference) || '\u7537\u9891'); const [wordRange, setWordRange] = useState(() => (localStorage.getItem('scriptflow_word_range') as WordRange) || '\u4e0d\u9650'); // Conversion Tool States - const [sourceText, setSourceText] = useState(() => localStorage.getItem('scriptflow_conversion_source') || '在一个偏远的小山村里,住着一个叫阿强的小伙子。阿强从小就有一个梦想,那就是去大城市闯荡。有一天,他在山里救了一个受伤的老人,老人临走前送给他一个神秘的木盒子,告诉他只有在最绝望的时候才能打开。阿强带着盒子来到了繁华的都市,却发现这里比他想象的要残酷得多...'); + const [sourceText, setSourceText] = useState(() => localStorage.getItem('scriptflow_conversion_source') || '\u4e3b\u89d2\u5728\u96e8\u591c\u63a5\u5230\u4e00\u901a\u795e\u79d8\u7535\u8bdd\uff0c\u53d1\u73b0\u5bf9\u65b9\u7adf\u7136\u662f\u5341\u5e74\u524d\u5931\u8e2a\u7684\u7236\u4eb2\u3002'); const [isConverting, setIsConverting] = useState(false); const [scripts, setScripts] = useState(() => { const saved = localStorage.getItem('scriptflow_conversion_scripts'); @@ -363,6 +611,8 @@ export default function App() { const [isExtractingBackground, setIsExtractingBackground] = useState(false); const [sourceExtractionError, setSourceExtractionError] = useState(''); const [backgroundExtractionError, setBackgroundExtractionError] = useState(''); + const [isExtractingCreationCharacters, setIsExtractingCreationCharacters] = useState(false); + const [creationCharacterExtractionError, setCreationCharacterExtractionError] = useState(''); const [sourceUploadName, setSourceUploadName] = useState(() => localStorage.getItem('scriptflow_conversion_source_upload_name') || ''); const [extractedEpisodes, setExtractedEpisodes] = useState(() => parseEpisodesFromText(localStorage.getItem('scriptflow_conversion_source') || '')); const [isFinalizing, setIsFinalizing] = useState(false); @@ -383,12 +633,63 @@ export default function App() { const [isSyncing, setIsSyncing] = useState(false); const [isGeneratingAll, setIsGeneratingAll] = useState(false); const [editingCreationScriptKey, setEditingCreationScriptKey] = useState(null); - const [finalizedFilter, setFinalizedFilter] = useState('全部'); + const [finalizedFilter, setFinalizedFilter] = useState('\u5b8c\u6574\u5267\u672c'); // Conversion Settings States const [showSettingsModal, setShowSettingsModal] = useState(false); const [showCreationGuardModal, setShowCreationGuardModal] = useState(false); const [creationGuardMessage, setCreationGuardMessage] = useState(''); + const [showAssetCenter, setShowAssetCenter] = useState(false); + const [assetCenterMode, setAssetCenterMode] = useState<'conversion' | 'creation'>('creation'); + const [assetCenterTab, setAssetCenterTab] = useState('portraits'); + const [selectedAssetCardIdsConversion, setSelectedAssetCardIdsConversion] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_selected_conversion'); + return parseSavedAssetSelectionBuckets(saved); + }); + const [selectedAssetCardIdsCreation, setSelectedAssetCardIdsCreation] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_selected_creation'); + return parseSavedAssetSelectionBuckets(saved); + }); + const [assetVisualStyle, setAssetVisualStyle] = useState('photorealistic'); + const [assetAspectRatio, setAssetAspectRatio] = useState<'auto' | '1:1' | '3:4' | '16:9'>('auto'); + const [assetResolution, setAssetResolution] = useState<'1K' | '2K' | '4K'>('1K'); + const [assetOutputFormat, setAssetOutputFormat] = useState<'png' | 'jpg'>('png'); + const [assetBatchCount, setAssetBatchCount] = useState<1 | 2 | 4>(1); + const [assetNegativePrompt, setAssetNegativePrompt] = useState(ASSET_STYLE_CONFIG.photorealistic.negative); + const [assetGenerationStatusBucketsConversion, setAssetGenerationStatusBucketsConversion] = useState({}); + const [assetGenerationStatusBucketsCreation, setAssetGenerationStatusBucketsCreation] = useState({}); + const [assetGenerationMessageBucketsConversion, setAssetGenerationMessageBucketsConversion] = useState({}); + const [assetGenerationMessageBucketsCreation, setAssetGenerationMessageBucketsCreation] = useState({}); + const [assetPosterCompositionGeneratingBucketsConversion, setAssetPosterCompositionGeneratingBucketsConversion] = useState>({}); + const [assetPosterCompositionGeneratingBucketsCreation, setAssetPosterCompositionGeneratingBucketsCreation] = useState>({}); + const [assetPosterCompositionMessageBucketsConversion, setAssetPosterCompositionMessageBucketsConversion] = useState({}); + const [assetPosterCompositionMessageBucketsCreation, setAssetPosterCompositionMessageBucketsCreation] = useState({}); + const [assetLastTaskIdBucketsConversion, setAssetLastTaskIdBucketsConversion] = useState({}); + const [assetLastTaskIdBucketsCreation, setAssetLastTaskIdBucketsCreation] = useState({}); + const [assetResultBucketsConversion, setAssetResultBucketsConversion] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_results_conversion'); + return parseSavedAssetBuckets(saved); + }); + const [assetResultBucketsCreation, setAssetResultBucketsCreation] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_results_creation'); + return parseSavedAssetBuckets(saved); + }); + const [assetAppearanceDraftsConversion, setAssetAppearanceDraftsConversion] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_appearance_conversion'); + return parseSavedAssetTextBuckets(saved); + }); + const [assetAppearanceDraftsCreation, setAssetAppearanceDraftsCreation] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_appearance_creation'); + return parseSavedAssetTextBuckets(saved); + }); + const [assetPosterPromptDraftsConversion, setAssetPosterPromptDraftsConversion] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_poster_prompt_conversion'); + return parseSavedAssetTextBuckets(saved); + }); + const [assetPosterPromptDraftsCreation, setAssetPosterPromptDraftsCreation] = useState(() => { + const saved = localStorage.getItem('scriptflow_asset_poster_prompt_creation'); + return parseSavedAssetTextBuckets(saved); + }); const [conversionWorldview, setConversionWorldview] = useState(() => localStorage.getItem('scriptflow_conversion_worldview') || ''); const [conversionOutline, setConversionOutline] = useState(() => localStorage.getItem('scriptflow_conversion_outline') || ''); const [conversionCharacterProfiles, setConversionCharacterProfiles] = useState(() => localStorage.getItem('scriptflow_conversion_characterProfiles') || ''); @@ -416,6 +717,41 @@ export default function App() { localStorage.setItem('scriptflow_word_range', wordRange); }, [audiencePreference, wordRange]); + useEffect(() => { + setAssetNegativePrompt(ASSET_STYLE_CONFIG[assetVisualStyle].negative); + }, [assetVisualStyle]); + + useEffect(() => { + localStorage.setItem('scriptflow_asset_selected_conversion', JSON.stringify(selectedAssetCardIdsConversion)); + }, [selectedAssetCardIdsConversion]); + + useEffect(() => { + localStorage.setItem('scriptflow_asset_selected_creation', JSON.stringify(selectedAssetCardIdsCreation)); + }, [selectedAssetCardIdsCreation]); + + useEffect(() => { + localStorage.setItem('scriptflow_asset_results_conversion', JSON.stringify(assetResultBucketsConversion)); + }, [assetResultBucketsConversion]); + + useEffect(() => { + localStorage.setItem('scriptflow_asset_results_creation', JSON.stringify(assetResultBucketsCreation)); + }, [assetResultBucketsCreation]); + + + useEffect(() => { + localStorage.setItem('scriptflow_asset_appearance_conversion', JSON.stringify(assetAppearanceDraftsConversion)); + }, [assetAppearanceDraftsConversion]); + + useEffect(() => { + localStorage.setItem('scriptflow_asset_appearance_creation', JSON.stringify(assetAppearanceDraftsCreation)); + }, [assetAppearanceDraftsCreation]); + useEffect(() => { + localStorage.setItem('scriptflow_asset_poster_prompt_conversion', JSON.stringify(assetPosterPromptDraftsConversion)); + }, [assetPosterPromptDraftsConversion]); + + useEffect(() => { + localStorage.setItem('scriptflow_asset_poster_prompt_creation', JSON.stringify(assetPosterPromptDraftsCreation)); + }, [assetPosterPromptDraftsCreation]); // Persistence effect for Content useEffect(() => { localStorage.setItem('scriptflow_conversion_source', sourceText); @@ -482,11 +818,11 @@ export default function App() { const hydrateRemoteDraft = (draft: Record) => { setScriptType((draft.scriptType as ScriptType) || '\u77ed\u5267'); - setScriptFormat((draft.scriptFormat as ScriptFormat) || '全部'); - setSelectedThemes(Array.isArray(draft.selectedThemes) ? draft.selectedThemes as ThemeLabel[] : ['悬疑']); + setScriptFormat((draft.scriptFormat as ScriptFormat) || '\u5b8c\u6574\u5267\u672c'); + setSelectedThemes(Array.isArray(draft.selectedThemes) ? draft.selectedThemes as ThemeLabel[] : ['\u60ac\u7591']); setSelectedNarratives(Array.isArray(draft.selectedNarratives) ? draft.selectedNarratives as NarrativeMethod[] : ['\u987a\u53d9']); - setAudiencePreference((draft.audiencePreference as AudiencePreference) || '男频'); - setWordRange((draft.wordRange as WordRange) || '不限'); + setAudiencePreference((draft.audiencePreference as AudiencePreference) || '\u7537\u9891'); + setWordRange((draft.wordRange as WordRange) || '200 - 500'); setSourceText(typeof draft.sourceText === 'string' ? draft.sourceText : ''); setSourceUploadName(typeof draft.sourceUploadName === 'string' ? draft.sourceUploadName : ''); setExtractedEpisodes(Array.isArray(draft.extractedEpisodes) ? draft.extractedEpisodes as ExtractedEpisode[] : []); @@ -709,7 +1045,7 @@ export default function App() { const sourceEpisodes = extractedEpisodes.length > 0 ? extractedEpisodes - : [{ id: 'episode-0', title: '剧本内容', content: sourceText, wordCount: countWords(sourceText) }]; + : [{ id: 'episode-0', title: '\u7b2c1\u96c6', content: sourceText, wordCount: countWords(sourceText) }]; const canResume = !restartAll && conversionEpisodeResults.length === sourceEpisodes.length && conversionEpisodeResults.every((episode, index) => episode.id === sourceEpisodes[index]?.id); @@ -847,7 +1183,7 @@ export default function App() { } const latestEpisode = conversionEpisodeResultsRef.current.find((item) => item.id === episode.id) ?? existingEpisode; - const message = error instanceof Error ? error.message : '转换失败'; + const message = error instanceof Error ? error.message : '\u751f\u6210\u5931\u8d25'; const failedEpisode: ConversionEpisodeResult = { ...latestEpisode, status: 'failed' as const, @@ -905,9 +1241,8 @@ export default function App() { scriptToFinalize = finalizedPages.map((p) => { const script = p.scripts[p.selectedScriptIndex!]; const originalIndex = creationPages.indexOf(p); - return `## 第${originalIndex + 1}集 + return `## \u7b2c${originalIndex + 1}\u96c6\n\n${script}`; -${script}`; }).join('\n\n---\n\n'); } @@ -1095,15 +1430,14 @@ ${script}`; const exportAllScripts = () => { const fullScript = creationPages.map((p, i) => { const script = p.selectedScriptIndex !== null ? p.scripts[p.selectedScriptIndex] : (p.scripts[0] || '\u672a\u751f\u6210'); - return `## 第${i + 1}集 + return `## \u7b2c${i + 1}\u96c6\n\n${script}`; -${script}`; }).join('\n\n---\n\n'); const blob = new Blob([fullScript], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `ScriptFlow_剧本_${new Date().getTime()}.md`; + a.download = `ScriptFlow_\u5267\u672c_${new Date().getTime()}.md`; a.click(); URL.revokeObjectURL(url); }; @@ -1161,11 +1495,448 @@ ${script}`; setIsCreationCharacterProfilesEditing(value); }; + const setActiveCharacterProfiles = (value: string) => { + if (activeTab === 'conversion') { + setConversionCharacterProfiles(value); + return; + } + setCreationCharacterProfiles(value); + }; + + const updateCharacterProfileCards = (updater: (cards: CharacterPreviewCard[]) => CharacterPreviewCard[]) => { + const nextCards = updater(characterProfileCards); + setActiveCharacterProfiles(serializeCharacterProfileCards(nextCards)); + }; + + const addCharacterProfileCard = () => { + updateCharacterProfileCards((prev) => [...prev, createEmptyCharacterCard(prev.length)]); + }; + + const deleteCharacterProfileCard = (cardId: string) => { + updateCharacterProfileCards((prev) => prev.filter((card) => card.id !== cardId)); + }; + + const handleExtractCreationCharacters = async () => { + const outline = creationOutline.trim(); + if (!outline) { + showCreationBackgroundGuard('\u8bf7\u5148\u586b\u5199\u300c\u6545\u4e8b\u5927\u7eb2\u300d\uff0c\u518d\u63d0\u53d6\u89d2\u8272\u4fe1\u606f\u3002'); + return; + } + const previousProfiles = creationCharacterProfiles; + let latestCharacters = ''; + setCreationCharacterExtractionError(''); + setIsExtractingCreationCharacters(true); + try { + await extractPlotBackgroundFromSource(outline, (fields) => { + const nextCharacters = (fields.characters || '').trim(); + if (!nextCharacters) return; + latestCharacters = nextCharacters; + setCreationCharacterProfiles(nextCharacters); + }, { model: 'doubao', apiKey: aiApiKey }); + if (!latestCharacters) { + setCreationCharacterExtractionError('\u672a\u63d0\u53d6\u5230\u89d2\u8272\u4fe1\u606f\uff0c\u8bf7\u8865\u5145\u66f4\u5177\u4f53\u7684\u6545\u4e8b\u5927\u7eb2\u540e\u91cd\u8bd5\u3002'); + } + setIsCreationCharacterProfilesEditing(true); + } catch (error) { + setCreationCharacterProfiles(previousProfiles); + setCreationCharacterExtractionError(error instanceof Error ? error.message : '\u89d2\u8272\u63d0\u53d6\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5'); + } finally { + setIsExtractingCreationCharacters(false); + } + }; + const showCreationBackgroundGuard = (message: string) => { setCreationGuardMessage(message); setShowCreationGuardModal(true); }; + const openAssetCenter = (mode: 'conversion' | 'creation') => { + setAssetCenterMode(mode); + setAssetCenterTab('portraits'); + setShowAssetCenter(true); + }; + + const assetCards = parseCharacterProfileCards(assetCenterMode === 'conversion' ? conversionCharacterProfiles : creationCharacterProfiles); + const selectedAssetCardIdsByTab = assetCenterMode === 'conversion' ? selectedAssetCardIdsConversion : selectedAssetCardIdsCreation; + const selectedAssetCardIds = selectedAssetCardIdsByTab[assetCenterTab] || []; + const assetResultBuckets = assetCenterMode === 'conversion' ? assetResultBucketsConversion : assetResultBucketsCreation; + const assetResultBucketKey = getAssetBucketKey(assetCenterTab, selectedAssetCardIds); + const assetResultUrls = assetResultBuckets[assetResultBucketKey] || []; + const selectedAssetCards = assetCards.filter((card) => selectedAssetCardIds.includes(card.id)); + const assetAppearanceDrafts = assetCenterMode === 'conversion' ? assetAppearanceDraftsConversion : assetAppearanceDraftsCreation; + const assetGenerationStatusBuckets = assetCenterMode === 'conversion' ? assetGenerationStatusBucketsConversion : assetGenerationStatusBucketsCreation; + const assetGenerationMessageBuckets = assetCenterMode === 'conversion' ? assetGenerationMessageBucketsConversion : assetGenerationMessageBucketsCreation; + const assetPosterCompositionGeneratingBuckets = assetCenterMode === 'conversion' ? assetPosterCompositionGeneratingBucketsConversion : assetPosterCompositionGeneratingBucketsCreation; + const assetPosterCompositionMessageBuckets = assetCenterMode === 'conversion' ? assetPosterCompositionMessageBucketsConversion : assetPosterCompositionMessageBucketsCreation; + const assetLastTaskIdBuckets = assetCenterMode === 'conversion' ? assetLastTaskIdBucketsConversion : assetLastTaskIdBucketsCreation; + const assetGenerationStatus = assetGenerationStatusBuckets[assetResultBucketKey] || 'idle'; + const assetGenerationMessage = assetGenerationMessageBuckets[assetResultBucketKey] || ''; + const assetPosterCompositionGenerating = assetPosterCompositionGeneratingBuckets[assetResultBucketKey] || false; + const assetPosterCompositionMessage = assetPosterCompositionMessageBuckets[assetResultBucketKey] || ''; + const assetLastTaskId = assetLastTaskIdBuckets[assetResultBucketKey] || ''; + const defaultAppearanceDraft = selectedAssetCards.map((card) => { + const appearanceField = card.fields.find((field) => field.label === '\u5916\u8c8c'); + return '\u89d2\u8272:' + card.title + '\\n\u5916\u8c8c:' + (appearanceField?.value || '\u5f85\u8865\u5145'); + }).join('\n\n'); + const assetAppearanceDraft = assetAppearanceDrafts[assetResultBucketKey] ?? defaultAppearanceDraft; + const assetPosterPromptDrafts = assetCenterMode === 'conversion' ? assetPosterPromptDraftsConversion : assetPosterPromptDraftsCreation; + const posterRoleNames = selectedAssetCards.map((card) => card.title).filter(Boolean).join(' / '); + const modeOutline = assetCenterMode === 'conversion' ? conversionOutline : creationOutline; + const defaultPosterPromptFields: PosterPromptFields = { + title: '\u77ed\u5267\u300a\u672a\u547d\u540d\u300b', + hook: '\u591c\u8272\u4e0b\u7684\u8ffd\u51fb\u3001\u8c0e\u8a00\u4e0e\u771f\u76f8\u7ec8\u5c06\u76f8\u649e\u3002', + visual: '\u51b7\u6696\u5bf9\u6bd4\u3001\u96e8\u591c\u9006\u5149\u3001\u60ac\u7591\u7d27\u5f20\u611f\u3001\u7535\u5f71\u7ea7\u8d28\u611f', + cast: posterRoleNames || '\u672a\u9009\u62e9', + ratio: '16:9', + composition: '', + characterStyle: '\u771f\u4eba\u5199\u5b9e', + }; + const savedPosterDraft = assetPosterPromptDrafts[assetResultBucketKey] ?? ''; + const assetPosterPromptFields = parsePosterPromptFieldsFromDraft(savedPosterDraft, defaultPosterPromptFields); + + + const setSelectedAssetCardIds = (updater: (prev: string[]) => string[]) => { + if (assetCenterMode === 'conversion') { + setSelectedAssetCardIdsConversion((prev) => ({ + ...prev, + [assetCenterTab]: updater(prev[assetCenterTab] || []) + })); + return; + } + setSelectedAssetCardIdsCreation((prev) => ({ + ...prev, + [assetCenterTab]: updater(prev[assetCenterTab] || []) + })); + }; + + const setAssetGenerationStatusByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + status: AssetGenerationStatus + ) => { + if (mode === 'conversion') { + setAssetGenerationStatusBucketsConversion((prev) => ({ ...prev, [bucketKey]: status })); + return; + } + setAssetGenerationStatusBucketsCreation((prev) => ({ ...prev, [bucketKey]: status })); + }; + + const setAssetGenerationMessageByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + message: string + ) => { + if (mode === 'conversion') { + setAssetGenerationMessageBucketsConversion((prev) => ({ ...prev, [bucketKey]: message })); + return; + } + setAssetGenerationMessageBucketsCreation((prev) => ({ ...prev, [bucketKey]: message })); + }; + + const setAssetTaskIdByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + taskId: string + ) => { + if (mode === 'conversion') { + setAssetLastTaskIdBucketsConversion((prev) => ({ ...prev, [bucketKey]: taskId })); + return; + } + setAssetLastTaskIdBucketsCreation((prev) => ({ ...prev, [bucketKey]: taskId })); + }; + + const setAssetPosterCompositionGeneratingByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + value: boolean + ) => { + if (mode === 'conversion') { + setAssetPosterCompositionGeneratingBucketsConversion((prev) => ({ ...prev, [bucketKey]: value })); + return; + } + setAssetPosterCompositionGeneratingBucketsCreation((prev) => ({ ...prev, [bucketKey]: value })); + }; + + const setAssetPosterCompositionMessageByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + message: string + ) => { + if (mode === 'conversion') { + setAssetPosterCompositionMessageBucketsConversion((prev) => ({ ...prev, [bucketKey]: message })); + return; + } + setAssetPosterCompositionMessageBucketsCreation((prev) => ({ ...prev, [bucketKey]: message })); + }; + + const setAssetResultUrlsByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + updater: (prev: string[]) => string[] + ) => { + if (mode === 'conversion') { + setAssetResultBucketsConversion((prev) => ({ ...prev, [bucketKey]: updater(prev[bucketKey] || []) })); + return; + } + setAssetResultBucketsCreation((prev) => ({ ...prev, [bucketKey]: updater(prev[bucketKey] || []) })); + }; + + + const setAssetAppearanceDraftByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + value: string + ) => { + if (mode === 'conversion') { + setAssetAppearanceDraftsConversion((prev) => ({ ...prev, [bucketKey]: value })); + return; + } + setAssetAppearanceDraftsCreation((prev) => ({ ...prev, [bucketKey]: value })); + }; + const setAssetPosterPromptDraftByContext = ( + mode: 'conversion' | 'creation', + bucketKey: string, + value: string + ) => { + if (mode === 'conversion') { + setAssetPosterPromptDraftsConversion((prev) => ({ ...prev, [bucketKey]: value })); + return; + } + setAssetPosterPromptDraftsCreation((prev) => ({ ...prev, [bucketKey]: value })); + }; + const updateAssetPosterPromptField = ( + fieldKey: PosterPromptFieldKey, + value: string + ) => { + const nextFields: PosterPromptFields = { ...assetPosterPromptFields, [fieldKey]: value }; + setAssetPosterPromptDraftByContext( + assetCenterMode, + assetResultBucketKey, + buildPosterPromptDraftFromFields(nextFields) + ); + }; + + const handleGeneratePosterComposition = async () => { + if (assetCenterTab !== 'poster' || assetPosterCompositionGenerating) return; + + const generationMode = assetCenterMode; + const generationBucketKey = getAssetBucketKey(assetCenterTab, selectedAssetCardIds); + const styleConfig = ASSET_STYLE_CONFIG[assetVisualStyle]; + const modeOutlineText = assetCenterMode === 'conversion' ? conversionOutline : creationOutline; + + try { + setAssetPosterCompositionGeneratingByContext(generationMode, generationBucketKey, true); + setAssetPosterCompositionMessageByContext(generationMode, generationBucketKey, '\u6b63\u5728\u751f\u6210\u6784\u56fe\u4e0e\u5149\u5f71...'); + + const result = await generatePosterCompositionSuggestion({ + title: assetPosterPromptFields.title, + hook: assetPosterPromptFields.hook, + visualKeywords: assetPosterPromptFields.visual, + castDescription: assetPosterPromptFields.cast, + compositionRatio: assetPosterPromptFields.ratio || '16:9', + outline: modeOutlineText || '', + styleLabel: `${styleConfig.label} / ${assetPosterPromptFields.characterStyle || '\u771f\u4eba\u5199\u5b9e'}`, + apiKey: aiApiKey, + onUpdate: (content) => { + const nextFields: PosterPromptFields = { ...assetPosterPromptFields, composition: content.trim() }; + setAssetPosterPromptDraftByContext(generationMode, generationBucketKey, buildPosterPromptDraftFromFields(nextFields)); + }, + }); + + const finalText = result.trim(); + if (finalText) { + const nextFields: PosterPromptFields = { ...assetPosterPromptFields, composition: finalText }; + setAssetPosterPromptDraftByContext(generationMode, generationBucketKey, buildPosterPromptDraftFromFields(nextFields)); + } + + setAssetPosterCompositionMessageByContext( + generationMode, + generationBucketKey, + finalText ? '\u6784\u56fe\u4e0e\u5149\u5f71\u751f\u6210\u5b8c\u6210' : '\u672a\u751f\u6210\u6709\u6548\u5185\u5bb9' + ); + } catch (error) { + setAssetPosterCompositionMessageByContext( + generationMode, + generationBucketKey, + error instanceof Error ? error.message : '\u6784\u56fe\u4e0e\u5149\u5f71\u751f\u6210\u5931\u8d25' + ); + } finally { + setAssetPosterCompositionGeneratingByContext(generationMode, generationBucketKey, false); + } + }; + + const toggleAssetCard = (cardId: string) => { + setSelectedAssetCardIds((prev) => prev.includes(cardId) ? prev.filter((id) => id !== cardId) : [...prev, cardId]); + }; + + const handleGenerateAsset = async () => { + if (selectedAssetCardIds.length === 0 || assetGenerationStatus === 'generating') return; + + const generationMode = assetCenterMode; + const generationBucketKey = getAssetBucketKey(assetCenterTab, selectedAssetCardIds); + + const selectedCards = selectedAssetCards; + const roleText = selectedCards.map((card) => { + const fieldsForPrompt = assetCenterTab === 'portraits' + ? card.fields.filter((field) => PORTRAIT_FIELD_LABELS.has(field.label)) + : card.fields; + const detail = fieldsForPrompt.map((field) => `${field.label}:${field.value}`).join('\n'); + + if (assetCenterTab === 'portraits') { + return ''; + } + + return `${card.title}\n${detail}${card.rawContent ? `\n${card.rawContent}` : ''}`; + }).filter(Boolean).join('\n\n'); + const portraitAppearanceText = assetCenterTab === 'portraits' ? (assetAppearanceDraft.trim() || defaultAppearanceDraft || '\u5916\u8c8c:\u5f85\u8865\u5145') : ''; + + const portraitRenderConstraint = assetCenterTab === 'portraits' + ? '\u51fa\u56fe\u7ea6\u675f\uff1a\u767d\u5e95\uff0c\u89d2\u8272\u5f62\u8c61\u8bbe\u8ba1\u56fe\uff0c\u5355\u4eba\u7ad6\u6784\u56fe\uff0c\u4e0d\u8981\u573a\u666f\u73af\u5883\uff0c\u4e0d\u8981\u9053\u5177\u80cc\u666f\uff0c\u4e0d\u8981\u6587\u5b57\uff0c\u4fdd\u7559\u5b8c\u6574\u4eba\u7269\u7ec6\u8282\u3002' + : ''; + const taskLabel = assetCenterTab === 'portraits' ? '\u89d2\u8272\u5f62\u8c61' : assetCenterTab === 'relations' ? '\u89d2\u8272\u5173\u7cfb\u56fe' : '\u5267\u672c\u6d77\u62a5'; + const styleConfig = ASSET_STYLE_CONFIG[assetVisualStyle]; + const styleLabel = styleConfig.label; + const stylePrompt = styleConfig.prompt; + const isRelationTask = assetCenterTab === 'relations'; + const isPosterTask = assetCenterTab === 'poster'; + + const referenceCards = (isRelationTask || isPosterTask) + ? selectedCards + .map((card) => { + const portraitKey = getAssetBucketKey('portraits', [card.id]); + const portraitUrl = assetResultBuckets[portraitKey]?.[0] || ''; + return { card, portraitUrl }; + }) + .filter((item) => Boolean(item.portraitUrl)) + .slice(0, RELATION_REFERENCE_MAX) + : []; + + const referenceImageInput = referenceCards.map((item) => item.portraitUrl); + const referenceImageIndexMap = referenceCards + .map((item, index) => `IMG${String(index + 1).padStart(2, '0')}=${item.card.title}`) + .join('\n'); + + const relationSummaryText = selectedCards + .map((card) => { + const identity = card.fields.find((field) => field.label === '\u8eab\u4efd')?.value || ''; + const personality = card.fields.find((field) => field.label === '\u6027\u683c')?.value || ''; + const goal = card.fields.find((field) => field.label === '\u76ee\u6807')?.value || ''; + const conflict = card.fields.find((field) => field.label === '\u51b2\u7a81')?.value || ''; + const relation = card.fields.find((field) => field.label === '\u5173\u7cfb')?.value || ''; + const summary = [identity, personality, goal, conflict, relation].filter(Boolean).join('\uff1b'); + return `- ${card.title}\uff1a${summary || card.rawContent || '\u5f85\u8865\u5145'}`; + }) + .join('\n'); + + const posterPromptFieldsForTask: PosterPromptFields = { ...assetPosterPromptFields }; + const posterCharacterStyle = posterPromptFieldsForTask.characterStyle || '\u771f\u4eba\u5199\u5b9e'; + const posterCharacterStylePrompt = getPosterCharacterStylePrompt(posterCharacterStyle); + + const relationPrompt = [ + '\u8fd9\u51e0\u5f20\u662f\u4e3b\u8981\u89d2\u8272\u7684\u5f62\u8c61\uff0c\u751f\u6210\u4e00\u5f20\u5305\u542b\u6240\u6709\u89d2\u8272\u7684\u4eba\u7269\u5173\u7cfb\u56fe\uff0c\u5173\u7cfb\u56fe\u4e2d\u51fa\u73b0\u4e0a\u8ff0\u5173\u952e\u4eba\u7269\uff0c\u9700\u8981\u8d34\u4e0a\u4eba\u7269\u5934\u50cf\u56fe\uff0c\u4e00\u4e2a\u4eba\u7269\u53ea\u80fd\u51fa\u73b0\u4e00\u6b21\uff0c\u5173\u7cfb\u56fe\u4e2d\u51fa\u73b0\u7684\u6587\u5b57\u6807\u6ce8\u90fd\u7528\u4e2d\u6587\u6807\u6ce8\uff0c\u80cc\u666f\u98ce\u683c\u4e3a\u5211\u4fa6\u529e\u6848\u5173\u7cfb\u56fe\u6216\u52a8\u6f2b\u5730\u56fe\u7c7b\u578b\u3002', + '\u8bf7\u5728\u751f\u6210\u56fe\u7247\u524d\u5148\u51c6\u786e\u5206\u6790\u4eba\u7269\u4e4b\u95f4\u5173\u7cfb\uff0c\u7ed9\u51fa\u7b80\u6d01\u5173\u7cfb\u63cf\u8ff0\uff0c\u6700\u7ec8\u4efb\u52a1\u662f\u751f\u6210\u4e00\u5f20\u4f53\u73b0\u4eba\u7269\u5173\u7cfb\u7684\u56fe\u7247\u3002', + '\u89d2\u8272\u5934\u50cf\u5f15\u7528\u6620\u5c04\uff08data \u4e2d\u56fe\u7247\u5bf9\u5e94\u89d2\u8272\uff09\uff1a', + referenceImageIndexMap || '\u65e0', + '\u5173\u7cfb\u7b80\u8ff0\uff1a', + relationSummaryText || '\u5f85\u8865\u5145', + '\u4ee5\u4e0b\u662f\u5267\u672c\u7684\u4eba\u7269\u5173\u7cfb\u6570\u636e\uff1a', + roleText || relationSummaryText || '\u5f85\u8865\u5145', + ].join('\n\n'); + + const posterPrompt = [ + '\u4f60\u7684\u4efb\u52a1\u662f\u751f\u6210\u4e00\u5f20\u53ef\u7528\u4e8e\u5ba3\u53d1\u7684\u77ed\u5267\u7535\u5f71\u7ea7\u6d77\u62a5\u3002', + '\u8bf7\u4f7f\u7528\u63d0\u4f9b\u7684\u89d2\u8272\u53c2\u8003\u5934\u50cf\uff0c\u4fdd\u6301\u4eba\u7269\u4e00\u81f4\u6027\uff0c\u6bcf\u4e2a\u4eba\u7269\u53ea\u51fa\u73b0\u4e00\u6b21\uff0c\u4e0d\u8981\u65b0\u589e\u672a\u63d0\u4f9b\u7684\u89d2\u8272\u3002', + '\u89d2\u8272\u53c2\u8003\u6620\u5c04\uff1a', + referenceImageIndexMap || '\u65e0', + '\u6d77\u62a5\u521b\u610f\u8bf4\u660e\uff08\u4ee5\u4e0b\u6587\u5b57\u9700\u8981\u4f53\u73b0\u5728\u753b\u9762\u8bbe\u8ba1\u4e2d\uff09\uff1a', + posterPromptTextFromFields(posterPromptFieldsForTask), + '\u8bbe\u8ba1\u89c4\u8303\uff1a\u4e2d\u6587\u6807\u9898\u5b57\u4f53\u9ad8\u7ea7\u3001\u5b57\u4f53\u6392\u7248\u6e05\u6670\u3001\u4e3b\u89d2\u4eba\u7269\u5c45\u4e2d\u6216\u4e09\u89d2\u6784\u56fe\u3001\u5f3a\u70c8\u5149\u5f71\u5bf9\u6bd4\u3001\u5c42\u6b21\u5206\u660e\u3001\u8d28\u611f\u7ec6\u8282\u9ad8\u6e05\u3002', + '\u6587\u5b57\u8981\u6c42\uff1a\u6d77\u62a5\u4e2d\u7684\u6240\u6709\u6587\u6848\u90fd\u4f7f\u7528\u4e2d\u6587\uff0c\u4e0d\u51fa\u73b0\u82f1\u6587\u4e71\u7801\u3002', + '\u98ce\u683c\u4e0e\u8c03\u8272\uff1a' + styleLabel + '\uff0c' + stylePrompt, + '\u5267\u60c5\u53c2\u8003\uff1a' + (modeOutline || '\u5f85\u8865\u5145'), + assetNegativePrompt.trim() ? ('\u8d1f\u5411\u63d0\u793a\uff1a' + assetNegativePrompt.trim()) : '', + '\u6700\u7ec8\u8f93\u51fa\u76ee\u6807\uff1a\u4e00\u5f20\u6781\u5177\u89c6\u89c9\u51b2\u51fb\u529b\u7684\u5b8c\u6574\u5267\u672c\u6d77\u62a5\u3002' + ].filter(Boolean).join('\n\n'); + + const defaultPrompt = [ + `\u4efb\u52a1\uff1a${taskLabel}`, + `\u98ce\u683c\uff1a${styleLabel}`, + stylePrompt, + `\u6a21\u5f0f\uff1a${assetCenterMode === 'conversion' ? '\u4e00\u952e\u8f6c\u6362\u6a21\u5f0f' : '\u5373\u65f6\u521b\u4f5c\u6a21\u5f0f'}`, + `\u89d2\u8272:\n${assetCenterTab === 'portraits' ? portraitAppearanceText : roleText}`, + portraitRenderConstraint, + assetNegativePrompt.trim() ? `\u8d1f\u5411\u63d0\u793a\uff1a${assetNegativePrompt.trim()}` : '', + ].filter(Boolean).join('\n\n'); + const prompt = isRelationTask ? relationPrompt : isPosterTask ? posterPrompt : defaultPrompt; + + const requestAspectRatio = isRelationTask ? '16:9' : isPosterTask ? ((posterPromptFieldsForTask.ratio === '9:16' ? '9:16' : '16:9')) : assetAspectRatio; + const requestResolution = isRelationTask ? '2K' : isPosterTask ? '2K' : assetResolution; + const requestOutputFormat = isRelationTask ? 'png' : isPosterTask ? 'png' : assetOutputFormat; + const requestImageInput = (isRelationTask || isPosterTask) ? referenceImageInput : []; + + if ((isRelationTask || isPosterTask) && requestImageInput.length === 0) { + setAssetGenerationStatusByContext(generationMode, generationBucketKey, 'error'); + setAssetGenerationMessageByContext(generationMode, generationBucketKey, '\u8bf7\u5148\u4e3a\u5df2\u9009\u89d2\u8272\u751f\u6210\u201c\u89d2\u8272\u5f62\u8c61\u201d\u56fe\uff0c\u518d\u751f\u6210\u5173\u7cfb\u56fe\u6216\u6d77\u62a5'); + return; + } + + try { + setAssetGenerationStatusByContext(generationMode, generationBucketKey, 'generating'); + setAssetGenerationMessageByContext(generationMode, generationBucketKey, '\u6b63\u5728\u63d0\u4ea4\u4efb\u52a1...'); + setAssetTaskIdByContext(generationMode, generationBucketKey, ''); + setAssetResultUrlsByContext(generationMode, generationBucketKey, () => []); // overwrite only current isolated bucket + + const callBackUrl = (import.meta.env.VITE_KIE_CALLBACK_URL as string | undefined) || undefined; + const allUrls: string[] = []; + + for (let batchIndex = 0; batchIndex < assetBatchCount; batchIndex += 1) { + setAssetGenerationMessageByContext(generationMode, generationBucketKey, `\u6b63\u5728\u63d0\u4ea4\u7b2c ${batchIndex + 1}/${assetBatchCount} \u4e2a\u4efb\u52a1...`); + const created = await createNanoBanana2Task({ + prompt, + imageInput: requestImageInput, + callBackUrl, + googleSearch: true, + aspectRatio: requestAspectRatio, + resolution: requestResolution, + outputFormat: requestOutputFormat, + }); + + setAssetTaskIdByContext(generationMode, generationBucketKey, created.taskId); + + const pollIntervalMs = KIE_POLL_INTERVAL_MS; + const maxAttempts = KIE_POLL_MAX_ATTEMPTS; + let success = false; + for (let i = 0; i < maxAttempts; i += 1) { + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + const detail = await getKieTaskDetail({ taskId: created.taskId }); + + if (detail.state === 'success') { + allUrls.push(...(detail.imageUrls || [])); + success = true; + setAssetResultUrlsByContext(generationMode, generationBucketKey, () => [...allUrls]); + break; + } + + if (detail.state === 'fail') { + throw new Error(detail.failMsg || '\u751f\u6210\u5931\u8d25'); + } + + setAssetGenerationMessageByContext(generationMode, generationBucketKey, `\u7b2c ${batchIndex + 1}/${assetBatchCount} \u4e2a\u4efb\u52a1\u751f\u6210\u4e2d\uff08${i + 1}/${maxAttempts}\uff09`); + } + + if (!success) { + throw new Error('\u8f6e\u8be2\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'); + } + } + + setAssetGenerationStatusByContext(generationMode, generationBucketKey, 'success'); + setAssetGenerationMessageByContext(generationMode, generationBucketKey, allUrls.length > 0 ? `\u751f\u6210\u6210\u529f\uff0c\u5171 ${allUrls.length} \u5f20` : '\u4efb\u52a1\u6210\u529f\uff0c\u4f46\u672a\u8fd4\u56de\u56fe\u7247\u94fe\u63a5'); + } catch (error) { + setAssetGenerationStatusByContext(generationMode, generationBucketKey, 'error'); + setAssetGenerationMessageByContext(generationMode, generationBucketKey, error instanceof Error ? error.message : '\u8c03\u7528\u5931\u8d25'); + } + }; + const handleShowFinalizedCreation = () => { const finalizedPages = creationPages.filter(p => p.selectedScriptIndex !== null); @@ -1178,9 +1949,8 @@ ${script}`; const fullScript = finalizedPages.map((p) => { const script = p.scripts[p.selectedScriptIndex!]; const originalIndex = creationPages.indexOf(p); - return `## 第${originalIndex + 1}集 + return `## \u7b2c${originalIndex + 1}\u96c6\n\n${script}`; -${script}`; }).join('\n\n---\n\n'); setFinalizedScripts((prev) => ({ ...prev, creation: fullScript })); setActiveTab('finalized'); @@ -1250,9 +2020,9 @@ ${script}`; const remoteSaveStatusLabel = currentUser ? (lastRemoteSaveAt - ? `????? ${new Date(lastRemoteSaveAt).toLocaleString()}` - : '????????????') - : '????????????'; + ? (`\u4e91\u7aef\u5df2\u4fdd\u5b58 ${new Date(lastRemoteSaveAt).toLocaleString()}`) + : '\u5c1a\u672a\u4e91\u7aef\u4fdd\u5b58') + : '\u672a\u767b\u5f55\uff0c\u4ec5\u672c\u5730\u4fdd\u5b58'; return (
@@ -1286,7 +2056,7 @@ ${script}`;
- 系统就绪 + {'\u7cfb\u7edf\u5c31\u7eea'}
@@ -1380,14 +2150,14 @@ ${script}`; )} > {isEditingFinalized ? : } - {isEditingFinalized ? '保存修改' : '直接修改定稿'} + {isEditingFinalized ? '\u5b8c\u6210\u7f16\u8f91' : '\u76f4\u63a5\u4fee\u6539\u5b9a\u7a3f'}
{/* Invisible bridge to prevent menu closing when moving cursor */}
@@ -1405,28 +2175,28 @@ ${script}`; className="w-full text-left px-4 py-3 text-[12px] font-medium text-[#1D1D1F] hover:bg-[#F5F5F7] flex items-center gap-2" > - 导出 Markdown (.md) + {'\u5bfc\u51fa Markdown (.md)'}
@@ -1529,7 +2299,7 @@ ${script}`; {/* Left Sidebar: Scene Navigation */}
- 场景目录 (Scenes) + {'\u573a\u666f\u76ee\u5f55 (SCENES)'}
{sidebarItems.map((item, idx) => ( @@ -1573,7 +2343,7 @@ ${script}`; e.target.style.height = e.target.scrollHeight + 'px'; }} className="w-full min-h-[900px] bg-transparent border-0 outline-none focus:ring-0 text-[#1D1D1F] resize-none font-mono text-[14px] leading-[1.6] relative z-10 overflow-hidden break-words whitespace-pre-wrap" - placeholder="在此直接修改定稿剧本内容..." + placeholder={'\u5728\u8fd9\u91cc\u7f16\u8f91\u5b9a\u7a3f\u5267\u672c\u5185\u5bb9...'} autoFocus />
@@ -1666,12 +2436,12 @@ ${script}`;
- {`源文剧本管理`} + {'\u6e90\u6587\u5267\u672c\u7ba1\u7406'}
{(isExtractingSource || isExtractingBackground || extractedEpisodes.length > 0) && ( - {countWords(sourceText)}{`字`} + {countWords(sourceText)}{'\u5b57'} )} - {`删除全部`} + {'\u5220\u9664\u5168\u90e8'} )}
- {`支持 Word / TXT / PDF / MD,解析后以剧集卡片形式管理`} - {`标题展示格式:第几集:集名称,集名称会根据文件内容自动识别,可为空。`} + {'\u652f\u6301 Word / TXT / PDF / MD\uff0c\u89e3\u6790\u540e\u4ee5\u5267\u96c6\u5361\u7247\u5f62\u5f0f\u7ba1\u7406'} + {'\u6807\u9898\u5c55\u793a\u683c\u5f0f\uff1a\u7b2c\u51e0\u96c6\uff1a\u96c6\u540d\u79f0\uff0c\u96c6\u540d\u79f0\u4f1a\u6839\u636e\u6587\u4ef6\u5185\u5bb9\u81ea\u52a8\u8bc6\u522b\uff0c\u53ef\u4e3a\u7a7a\u3002'}
{sourceUploadName && ( {sourceUploadName} @@ -1720,11 +2490,11 @@ ${script}`;
-

{`上传剧本文件`}

-

{`上传后会自动解析文件,流式提取每一集的原始剧本内容,并以卡片形式展示。`}

+

{'\u4e0a\u4f20\u5267\u672c\u6587\u4ef6'}

+

{'\u4e0a\u4f20\u540e\u4f1a\u81ea\u52a8\u89e3\u6790\u6587\u4ef6\uff0c\u6d41\u5f0f\u63d0\u53d6\u6bcf\u4e00\u96c6\u7684\u539f\u59cb\u5267\u672c\u5185\u5bb9\uff0c\u5e76\u4ee5\u5361\u7247\u5f62\u5f0f\u5c55\u793a\u3002'}

- {`支持 docx / txt / pdf / md`} + {'\u652f\u6301 docx / txt / pdf / md'}
@@ -1796,16 +2566,16 @@ ${script}`;
- 生成参数设置 + {'\u751f\u6210\u53c2\u6570\u8bbe\u7f6e'}
@@ -1817,7 +2587,7 @@ ${script}`; className="px-6 py-6 space-y-4" >
- 剧本类型 + {'\u5267\u672c\u7c7b\u578b'}
{SCRIPT_TYPES.map((type) => (
- 核心风格 + {'\u6838\u5fc3\u98ce\u683c'}
- {(['悬疑', '侦探', '犯罪', '反转', '惊悚', '推理'] as ThemeLabel[]).map((t) => ( + {THEME_OPTIONS.map((t) => (
- 叙事手法 + {'\u53d9\u4e8b\u624b\u6cd5'}
{NARRATIVE_OPTIONS.map((n) => (
+
+ +
{activeTab === 'conversion' && (isExtractingBackground || backgroundExtractionError) && (

- {`正在逐集转换剧本...`} + {'\u6b63\u5728\u8f6c\u6362\u5267\u672c...'} ) : isExtractingSource && isExtractingBackground ? ( <> @@ -1969,17 +2748,17 @@ ${script}`; ) : isExtractingSource ? ( <> - {`正在提取剧集内容...`} + {'\u6b63\u5728\u63d0\u53d6\u5267\u672c\u5185\u5bb9...'} ) : isExtractingBackground ? ( <> - {`正在提取剧情背景...`} + {'\u6b63\u5728\u63d0\u53d6\u5267\u60c5\u80cc\u666f...'} ) : ( <> - {hasResumableConversion ? `继续逐集生成` : `立即转换成剧本`} + {hasResumableConversion ? '\u7ee7\u7eed\u8f6c\u6362\u5267\u672c' : '\u7acb\u5373\u8f6c\u6362\u5267\u672c'} )} @@ -1989,7 +2768,7 @@ ${script}`; className="px-5 py-3.5 rounded-2xl bg-[#111111] text-white text-sm font-bold flex items-center gap-2 hover:bg-black transition-all shadow-lg shadow-black/15" > - 中止 + {'\u505c\u6b62\u8f6c\u6362'} )} {conversionEpisodeResults.length > 0 && ( @@ -1999,7 +2778,7 @@ ${script}`; className="px-8 py-3.5 rounded-2xl bg-emerald-500 text-white text-sm font-bold flex items-center gap-2 hover:bg-emerald-600 transition-all shadow-lg shadow-emerald-500/20 disabled:opacity-50" > {isFinalizing ? : } - 定稿 + {'\u5b9a\u7a3f\u5267\u672c'} )}

@@ -2025,15 +2804,15 @@ ${script}`;
-

逐集进度

+

{'\u5267\u96c6\u8f6c\u6362\u8fdb\u5ea6'}

{currentGeneratingEpisode - ? `正在生成 ${currentGeneratingEpisode.title}` + ? ('\u6b63\u5728\u751f\u6210 ' + currentGeneratingEpisode.title) : pausedConversionEpisode - ? `已暂停在 ${pausedConversionEpisode.title},可以继续生成` + ? ('\u5df2\u6682\u505c\u5728 ' + pausedConversionEpisode.title + '\uff0c\u53ef\u4ee5\u7f16\u8f91\u540e\u7ee7\u7eed') : conversionCompletedCount === conversionEpisodeResults.length - ? `\u5df2\u5b8c\u6210 ${conversionEpisodeResults.length} \u96c6\u8f6c\u6362` - : `\u5171 ${conversionEpisodeResults.length} \u96c6\u5f85\u8f6c\u6362`} + ? ('\u5df2\u5b8c\u6210 ' + conversionEpisodeResults.length + ' \u96c6\u8f6c\u6362') + : ('\u5171 ' + conversionEpisodeResults.length + ' \u96c6\u5f85\u8f6c\u6362')}

@@ -2082,8 +2861,8 @@ ${script}`;

{episode.title}

{episode.status === 'generating' && '\u6b63\u5728\u751f\u6210\u5f53\u524d\u96c6\u7684 A / B \u53cc\u7248\u672c'} - {episode.status === 'paused' && '当前剧集已中止,可编辑后继续生成'} - {episode.status === 'completed' && 'A / B 双版本已完成,可选择定稿'} + {episode.status === 'paused' && '\u5df2\u4e2d\u6b62\uff0c\u4f60\u53ef\u4ee5\u4fee\u6539\u540e\u7ee7\u7eed\u8f6c\u6362'} + {episode.status === 'completed' && 'A / B \u53cc\u7248\u672c\u5df2\u751f\u6210\uff0c\u53ef\u9009\u62e9\u5b9a\u7a3f'} {episode.status === 'failed' && (episode.error || '\u5f53\u524d\u96c6\u751f\u6210\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5')} {episode.status === 'pending' && '\u7b49\u5f85\u5f00\u59cb\u751f\u6210'}

@@ -2113,7 +2892,7 @@ ${script}`; episode.activeVersionId === version.id ? 'bg-white text-[#0071E3] shadow-sm' : 'text-[#86868B]' )} > - {`版本${version.label}`} + {`\u7248\u672c${version.label}`} {version.status === 'completed' ? '\u5df2\u5b8c\u6210' : version.status === 'generating' ? '\u751f\u6210\u4e2d' : version.status === 'paused' ? '\u5df2\u6682\u505c' : version.status === 'failed' ? '\u5931\u8d25' : '\u5f85\u751f\u6210'} @@ -2135,7 +2914,7 @@ ${script}`; onClick={() => setEditingConversionVersionKey((prev) => prev === activeVersionKey ? null : activeVersionKey)} className="px-3 py-1.5 rounded-xl text-[11px] font-bold transition-all border bg-white border-[#D2D2D7]/50 text-[#1D1D1F] hover:border-[#0071E3]/30" > - {isEditingActiveVersion ? '完成编辑' : '编辑剧本'} + {isEditingActiveVersion ? '\u5b8c\u6210\u7f16\u8f91' : '\u7f16\u8f91\u5267\u672c'} )} {episode.versions.map((version) => ( @@ -2161,7 +2940,7 @@ ${script}`;
-

{`版本 ${activeVersion.label}`}

+

{`\u7248\u672c ${activeVersion.label}`}

{activeVersion.status === 'generating' ? '\u6b63\u5728\u6d41\u5f0f\u751f\u6210\u5185\u5bb9' : activeVersion.status === 'completed' ? '\u5f53\u524d\u7248\u672c\u5df2\u751f\u6210\u5b8c\u6210' : activeVersion.status === 'paused' ? '\u5df2\u4e2d\u6b62\uff0c\u53ef\u4ece\u6b64\u7ee7\u7eed' : activeVersion.status === 'failed' ? '\u5f53\u524d\u7248\u672c\u751f\u6210\u5931\u8d25' : '\u7b49\u5f85\u5f00\u59cb\u751f\u6210'}

@@ -2189,7 +2968,7 @@ ${script}`;
) : (
- {activeVersion.status === 'generating' ? '正在生成内容...' : '暂无生成内容'} + {activeVersion.status === 'generating' ? '\u6b63\u5728\u751f\u6210\u5267\u672c...' : '\u7b49\u5f85\u751f\u6210\u5267\u672c'}
)}
@@ -2208,7 +2987,7 @@ ${script}`;
- 剧本类型 + {'\u5267\u672c\u7c7b\u578b'}
{SCRIPT_TYPES.map((type) => (
+
- 核心风格 + {'\u6838\u5fc3\u98ce\u683c'}
- {(['悬疑', '侦探', '犯罪', '反转', '惊悚', '推理'] as ThemeLabel[]).map((t) => ( + {THEME_OPTIONS.map((t) => (
- 叙事手法 + {'\u53d9\u4e8b\u624b\u6cd5'}
{NARRATIVE_OPTIONS.map((n) => (
@@ -2403,12 +3189,12 @@ ${script}`; {isSyncing ? ( <> - 正在生成... + {'\u540c\u6b65\u4e2d...'} ) : ( <> - 生成本集剧本 + {'\u751f\u6210\u5f53\u524d\u5267\u672c'} )} @@ -2550,6 +3336,332 @@ ${script}`;
)}
+ + {/* Asset Center Modal */} + + {showAssetCenter && ( +
+ +
+
+

{'\u521b\u4f5c\u8d44\u4ea7'}

+

{assetCenterMode === 'conversion' ? '\u4e00\u952e\u8f6c\u6362\u6a21\u5f0f' : '\u5373\u65f6\u521b\u4f5c\u6a21\u5f0f'}

+
+ +
+ +
+ {[ + { key: 'portraits', label: '\u89d2\u8272\u5f62\u8c61' }, + { key: 'relations', label: '\u89d2\u8272\u5173\u7cfb' }, + { key: 'poster', label: '\u5267\u672c\u6d77\u62a5' }, + ].map((tab) => ( + + ))} +
+ +
+ + +
+
+

+ {assetCenterTab === 'portraits' ? '\u89d2\u8272\u5f62\u8c61' : assetCenterTab === 'relations' ? '\u89d2\u8272\u5173\u7cfb' : '\u5267\u672c\u6d77\u62a5'} +

+

{'\u8d44\u4ea7\u9884\u89c8\u53ca\u751f\u6210\u5165\u53e3'}

+

+ {assetCenterTab === 'portraits' && '\u6839\u636e\u89d2\u8272\u5361\u81ea\u52a8\u7ec4\u88c5\u7ec6\u8282\u63cf\u8ff0\uff0c\u751f\u6210\u7edf\u4e00\u98ce\u683c\u7684\u89d2\u8272\u5f62\u8c61\u56fe\u3002'} + {assetCenterTab === 'relations' && '\u57fa\u4e8e\u89d2\u8272\u8eab\u4efd\u3001\u51b2\u7a81\u4e0e\u76ee\u6807\u5173\u7cfb\uff0c\u751f\u6210\u53ef\u89c6\u5316\u5173\u7cfb\u56fe\u3002'} + {assetCenterTab === 'poster' && '\u7ed3\u5408\u5267\u60c5\u5927\u7eb2\u4e0e\u89d2\u8272\u5361\uff0c\u751f\u6210\u5267\u672c\u5ba3\u53d1\u6d77\u62a5\u3002'} +

+
+

{'\u5df2\u9009\u89d2\u8272'}

+

{selectedAssetCardIds.length === 0 ? '\u672a\u9009\u62e9' : selectedAssetCardIds.map((id) => assetCards.find((card) => card.id === id)?.title || '').filter(Boolean).join(' / ')}

+
+ {assetCenterTab === 'portraits' && ( +
+

{'\u5916\u8c8c\u63cf\u8ff0'}

+

{'\u4ec5\u7528\u4e8e\u89d2\u8272\u5f62\u8c61\u751f\u6210\uff0c\u652f\u6301\u624b\u52a8\u7f16\u8f91'}

+