diff --git a/tests/test_story_lab_audio_ui.py b/tests/test_story_lab_audio_ui.py index 9829bc94..622b8670 100644 --- a/tests/test_story_lab_audio_ui.py +++ b/tests/test_story_lab_audio_ui.py @@ -19,6 +19,9 @@ def stories_ui() -> str: return "\n".join(path.read_text(encoding="utf-8") for path in sorted(STORIES.glob("*.tsx"))) +CATALOG_EN = ROOT / "ui" / "src" / "i18n" / "locales" / "en" / "storyLab.json" + + STORY_VIDEO_FORMAT = ROOT / "ui" / "src" / "features" / "stories" / "StoryVideoFormatControls.tsx" STORY_VIDEO_FORMAT_HELPERS = ROOT / "ui" / "src" / "features" / "stories" / "storyLabVideoFormat.ts" STORY_GALLERY = ROOT / "ui" / "src" / "features" / "stories" / "ReferenceGallery.tsx" @@ -152,12 +155,13 @@ def test_story_assets_support_reviewed_non_destructive_style_variants(): assert "approval: StoryApprovalState" in types assert "derivedFromAssetId?: string" in types assert "stylePrompt?: string" in types - assert "Convert selected images to a style" in panel + catalog = CATALOG_EN.read_text(encoding="utf-8") + assert "Convert selected images to a style" in catalog assert "QWEN_STYLE_EDIT_MODEL = 'qwen_image_edit_20B_gguf_q4_k_m'" in panel assert "FLUX_STYLE_EDIT_MODEL = 'flux2_klein_9b'" in panel - assert "Style conversion model" in panel - assert "MiniMax Image-01 · characters only" in panel - assert "Install selected local editor" in panel + assert "Style conversion model" in catalog + assert "MiniMax Image-01 · characters only" in catalog + assert "Install selected local editor" in catalog assert "Review and approve only the images Director should use" in panel assert "approval: item.approval === 'draft' ? 'draft' : 'approved'" in model @@ -175,9 +179,9 @@ def test_story_library_can_bulk_remove_only_selected_drafts(): assert "character.referenceAssetIds.filter" in deletion assert "delete current.assets[id]" in deletion assert "Generated files remain in Gallery" in deletion - assert "Delete selected Draft" in panel + assert "Delete selected Draft" in CATALOG_EN.read_text(encoding="utf-8") assert "visualAssetsNewestFirst" in panel - assert "Newest images appear first" in panel + assert "Newest images appear first" in CATALOG_EN.read_text(encoding="utf-8") def test_story_reference_images_confirm_removal_and_open_in_a_modal(): @@ -207,7 +211,7 @@ def test_story_style_converter_warns_about_photo_to_photo_noops_and_honors_reque assert "normalizedStyle" in prompt_builder assert ".replace(/\\s+/g, ' ')" in prompt_builder assert "Render only the visible wording explicitly requested" in prompt_builder - assert "a photorealistic remake will look almost unchanged" in panel + assert "a photorealistic remake will look almost unchanged" in CATALOG_EN.read_text(encoding="utf-8") def test_story_style_conversion_uses_true_qwen_edit_semantics_for_scenes(): @@ -229,7 +233,7 @@ def test_story_style_conversion_uses_flux_klein_as_a_true_four_step_image_editor assert "styleUsesFlux" in panel assert "styleUsesFlux ? 'flux' : 'qwen'" in panel - assert "fast 4-step edit" in panel + assert "fast 4-step edit" in CATALOG_EN.read_text(encoding="utf-8") assert "selected === 'flux2_klein_9b'" in generation assert "referenceParams.num_inference_steps = 4" in generation assert "referenceParams.guidance_scale = 1" in generation diff --git a/ui/scripts/check-i18n-catalogs.mjs b/ui/scripts/check-i18n-catalogs.mjs index 3369f3f2..27083152 100644 --- a/ui/scripts/check-i18n-catalogs.mjs +++ b/ui/scripts/check-i18n-catalogs.mjs @@ -46,6 +46,11 @@ const PILOT_FILES = [ 'src/features/workspaces/WorkspacesPanel.tsx', 'src/features/stories/storyLabChrome.tsx', 'src/features/stories/StoryOverviewTab.tsx', + 'src/features/stories/StoryAssetsTab.tsx', + 'src/features/stories/StoryAssetsImporter.tsx', + 'src/features/stories/StoryAssetsProposalCard.tsx', + 'src/features/stories/StoryAssetsStyleConverter.tsx', + 'src/features/stories/StoryAssetsLibrary.tsx', 'src/features/stories/StoryProviderPanel.tsx', 'src/features/stories/StoryProviderWritingFields.tsx', 'src/features/stories/StoryProviderImageFields.tsx', diff --git a/ui/src/features/stories/StoryAssetsImporter.tsx b/ui/src/features/stories/StoryAssetsImporter.tsx new file mode 100644 index 00000000..4f8bd836 --- /dev/null +++ b/ui/src/features/stories/StoryAssetsImporter.tsx @@ -0,0 +1,100 @@ +import { type RefObject } from 'react' +import { Check, Loader2, Upload } from 'lucide-react' +import { useUiTranslation } from '../../i18n' +import { button, panel, Field } from './storyLabChrome' +import { StoryAssetsProposalCard } from './StoryAssetsProposalCard' +import type { PendingSmartAsset } from './storyLabAssets' +import type { StoryProject } from './types' + +export function StoryAssetsImporter({ + project, smartAssetBusy, smartAssetDescription, setSmartAssetDescription, smartAssetRef, + pendingSmartAssets, setPendingSmartAssets, analyzeSmartAssets, applySmartAssets, patchPendingSmartAsset, +}: { + project: StoryProject + smartAssetBusy: boolean + smartAssetDescription: string + setSmartAssetDescription: (value: string) => void + smartAssetRef: RefObject + pendingSmartAssets: PendingSmartAsset[] + setPendingSmartAssets: (value: PendingSmartAsset[]) => void + analyzeSmartAssets: (files: File[]) => void + applySmartAssets: () => void + patchPendingSmartAsset: (index: number, patch: Partial) => void +}) { + const { t } = useUiTranslation('storyLab') + const analyzer = project.provider.writingProvider === 'maestro' + ? t('assets.analyzerInternal') + : t('assets.analyzerExternal', { + provider: project.provider.writingProvider, + model: project.provider.writingModel || t('assets.configuredModel'), + }) + return ( + <> +
+
+

{t('assets.title')}

+

{t('assets.description')}

+
+
+ {analyzer} +
+
+ +
+ +
+ +

{t('assets.batchContextHint')}

+
+
+ + {pendingSmartAssets.length > 0 && ( +
+
+
+

{t('assets.reviewTitle')}

+

{t('assets.reviewHint')}

+
+
+ + +
+
+
+ {pendingSmartAssets.map((item, index) => ( + + ))} +
+
+ )} + + ) +} diff --git a/ui/src/features/stories/StoryAssetsLibrary.tsx b/ui/src/features/stories/StoryAssetsLibrary.tsx new file mode 100644 index 00000000..4c8fc163 --- /dev/null +++ b/ui/src/features/stories/StoryAssetsLibrary.tsx @@ -0,0 +1,99 @@ +import { Check, Trash2 } from 'lucide-react' +import { useUiTranslation } from '../../i18n' +import { button, input, panel } from './storyLabChrome' +import type { StoryProject, StoryVisualAsset } from './types' + +export function StoryAssetsLibrary({ + project, styleAssetIds, setStyleAssetIds, selectedDraftAssetIds, styleConversionBusy, + deleteSelectedDraftAssets, toggleStyleAsset, patchVisualAsset, visualAssetsNewestFirst, +}: { + project: StoryProject + styleAssetIds: string[] + setStyleAssetIds: (ids: string[]) => void + selectedDraftAssetIds: string[] + styleConversionBusy: boolean + deleteSelectedDraftAssets: () => void + toggleStyleAsset: (id: string) => void + patchVisualAsset: (id: string, patch: Partial) => void + visualAssetsNewestFirst: StoryVisualAsset[] +}) { + const { t } = useUiTranslation('storyLab') + const assetIds = Object.keys(project.assets) + const approvedCount = Object.values(project.assets).filter(asset => asset.approval === 'approved').length + const allSelected = styleAssetIds.length === assetIds.length + return ( +
+
+
+

{t('assets.libraryTitle', { count: assetIds.length })}

+

+ {t('assets.libraryHint', { approved: approvedCount })} +

+
+ {assetIds.length > 0 && ( +
+ + +
+ )} +
+ {assetIds.length ? ( +
+ {visualAssetsNewestFirst.map(asset => ( +
+
+ {asset.name} + + + {asset.approval === 'approved' ? t('status.approved') : t('status.draft')} + +
+ patchVisualAsset(asset.id, { name: event.target.value })} + aria-label={t('assets.nameAria', { name: asset.name })} /> +
+ {asset.assetKind || asset.provider} + · + {asset.variantKind === 'styled' ? t('assets.styledVariant') : t('assets.original')} + {asset.model && <>·{asset.provider}/{asset.model}} + ·{new Date(asset.createdAt).toLocaleString()} +
+