Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/development/INTERNATIONALIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Typed JSON under `ui/src/i18n/locales/<lng>/<namespace>.json`.
| `activity` | Activity footer, Inbox / Legacy, Extra info inspector, Assets catalog chrome |
| `extraInfo` | Video Extra info dialog body (title stays `activity.extraInfo`) |
| `storyLab` | Story Lab section chrome, world/characters/relationships/structure tabs, reference gallery and editors |
| `director` | Director queue recovery and related chrome |
| `seriesLab` | Series Lab tabs, review assembly and shot-duration chrome |

Add a namespace only when a new product surface needs its own file. Do not grow a single giant JSON.

Expand Down
26 changes: 15 additions & 11 deletions docs/development/SLICE_QUEUE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Canonical sources in git:
Working notes under `comunicaciones/` are session handoff only. They are
gitignored and are not canonical.

## Landed on main (as of #98)
## Landed on main (as of #100)

Asset-manifest v1 writers: Studio generate (simulated, WGP, H3, SFX), Tools
upscale/revoice, Recast/Repaint/Outpaint, MiniMax image, Series assembly, 3D,
Expand All @@ -36,7 +36,8 @@ publish so Wizard→Studio→asset can share one durable identity.
focus), developerMode, sidebar, retake dialog. Slices bind through
`bindSlice` without `as never`. `developerModeSlice` no longer writes
`mediaFilter`; the facade still leaves `auditdev` when developer mode turns
off.
off. A parallel gallery/workspace slice PR may be in flight; it has not
landed on main.

Story Lab UI extracts:

Expand All @@ -46,7 +47,8 @@ Story Lab UI extracts:
- #91 Music, Trailer, Productions and Compact workspace, with `storyLab` EN+ES.
- #97 split those extracted tabs into smaller panels and added the code-health
PR table (`scripts/code_health.py --check --markdown`).
- #98 Overview + generation-agent panel, EN+ES. Assets remains in
- #98 Overview + generation-agent panel, EN+ES.
- #100 Assets tab extracted with EN/ES. Assets is no longer remaining in
`StoryLabPanel`.

i18n: foundation + Extra info inspector + Extra info video dialog (`extraInfo`
Expand All @@ -58,8 +60,8 @@ Recipe audio duration: generated audio is sized for its consumers (#93).
Character Kits / Face Rig / cutout dialogue HOWUSEIT: #94 on `main`. #26 was
the older Cursor docs pass and is closed as superseded.

`--markdown` without `--check` prints **Ratchet not evaluated.** CI uses
`--check --markdown`.
`--markdown` without `--check` prints **Ratchet not evaluated.** (#99). CI
uses `--check --markdown`.

## Next medium PRs

Expand All @@ -68,16 +70,18 @@ the older Cursor docs pass and is closed as superseded.
3. **Story Lab simple tabs** — landed (#88).
4. **Story Lab Music + Productions** — landed (#91, split further in #97).
5. **Story Lab Overview** — landed (#98).
6. **Story Lab Assets tab** — remaining chrome in `StoryLabPanel` (importer,
style conversion, visual library), EN+ES, no `useStore.ts` /
`_launch_runtime.py`.
7. **`useStore` slice** — one moderate cohesive extract with the public facade
6. **Story Lab Assets tab** — landed (#100).
7. **Story Lab assembly + library chrome** — remaining: extract Assembly tab
and leftover library chrome (header, tab labels, project types, prepare
buttons, nav notes) with EN/ES. No `useStore.ts` / `_launch_runtime.py`.
8. **`useStore` slice** — one moderate cohesive extract with the public facade
kept and `architectureSlices.test.mjs` extended. Do not move all of
`startGeneration` in one PR. At most one open PR may touch `useStore.ts`.
8. **Backend by domain**: one complete router + services per PR (Assets, Music,
A gallery/workspace slice may already be in flight; do not claim it landed.
9. **Backend by domain**: one complete router + services per PR (Assets, Music,
Series, Comics, …). Preserve route-table ordinals. Do not split
`_launch_runtime.py` by line count.
9. **Provenance applied by flow**: Studio+Wizard landed (#95). Remaining:
10. **Provenance applied by flow**: Studio+Wizard landed (#95). Remaining:
Story Lab+videoclip, Series+Comics. 3D+Director already has folder vs
Workspace provenance (#89). Do not start Director Paso 5 until a human
decides release order; that work must begin with 5.0 PipelineRuntime.
Expand Down
18 changes: 11 additions & 7 deletions tests/test_series_lab_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,23 @@ def test_review_is_thumbnail_first_and_exposes_ordered_editable_attempt_history(
assert "orderedTimelineShots" in review and "safeTimelineAttempt" in review
assert "Edit & regenerate" in review and "Save and regenerate in this slot" in review
assert "Join clips" in review and "startSeriesEpisodeAssembly" in review
assert "Montaje ordenado" in review and "Historial e intentos" in review
assert "t('review.orderedAssembly')" in review and "t('review.historyAttempts')" in review
assert "Attempts in this slot" in review
assert "Open complete approved sequence in Video Editor" in review


def test_story_productions_have_an_in_place_ordered_clip_timeline():
story = (ROOT / "ui" / "src" / "features" / "stories" / "StoryLabPanel.tsx").read_text(encoding="utf-8")
tabs = (ROOT / "ui" / "src" / "features" / "stories" / "storyLabTabs.ts").read_text(encoding="utf-8")
assembly = (ROOT / "ui" / "src" / "features" / "stories" / "StoryAssemblyTab.tsx").read_text(encoding="utf-8")
timeline = (ROOT / "ui" / "src" / "features" / "stories" / "StoryProductionTimeline.tsx").read_text(encoding="utf-8")
assert "StoryProductionTimeline" in story
assert "{ id: 'assembly', label: 'Montaje', icon: Play }" in story
assert "initiallyOpen={index === 0}" in story
assert "View ordered clips" in timeline and "Play all" in timeline
assert "Edit/regenerate clips" in timeline and "Join clips" in timeline
catalog_es = (ROOT / "ui" / "src" / "i18n" / "locales" / "es" / "storyLab.json").read_text(encoding="utf-8")
assert "id: 'assembly'" in tabs
assert "icon: Play" in tabs
assert "StoryProductionTimeline" in assembly
assert "initiallyOpen={index === 0}" in assembly
assert '"assembly": "Montaje"' in catalog_es
assert "t('timeline.viewOrdered')" in timeline and "t('timeline.playAll')" in timeline
assert "t('timeline.editRegenerate')" in timeline and "t('timeline.joinClips')" in timeline
assert "fetchSavedPipeline" in timeline


Expand Down
23 changes: 16 additions & 7 deletions tests/test_story_lab_trailer_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ROOT = Path(__file__).resolve().parents[1]
STORIES = ROOT / "ui" / "src" / "features" / "stories"
PANEL = STORIES / "StoryLabPanel.tsx"
TABS = STORIES / "storyLabTabs.ts"
TRAILER = STORIES / "StoryTrailerTab.tsx"
COMPACT = STORIES / "CompactVideoWorkspace.tsx"
VIDEO_FORMAT = STORIES / "storyLabVideoFormat.ts"
Expand All @@ -22,7 +23,7 @@
def story_lab_trailer_ui() -> str:
extracted = sorted(STORIES.glob("StoryTrailer*.tsx")) + sorted(STORIES.glob("Compact*.tsx"))
return "\n".join(path.read_text(encoding="utf-8") for path in (
PANEL, TRAILER, COMPACT, VIDEO_FORMAT, VIDEO_CONTROLS, CATALOG_EN, CATALOG_ES, *extracted,
PANEL, TABS, TRAILER, COMPACT, VIDEO_FORMAT, VIDEO_CONTROLS, CATALOG_EN, CATALOG_ES, *extracted,
))


Expand All @@ -38,13 +39,19 @@ def test_trailer_is_a_standalone_story_project_type_without_music():
types = TYPES.read_text(encoding="utf-8")
model = MODEL.read_text(encoding="utf-8")
panel = PANEL.read_text(encoding="utf-8")
tabs = TABS.read_text(encoding="utf-8")
catalog_en = CATALOG_EN.read_text(encoding="utf-8")
catalog_es = CATALOG_ES.read_text(encoding="utf-8")
backend = ROOT.joinpath("app", "_launch_runtime.py").read_text(encoding="utf-8")

assert "'full_story' | 'music_video' | 'trailer' | 'quick_video'" in types
assert "projectType === 'trailer' ? 60" in model
assert "{ id: 'trailer', label: 'Tráiler cinematográfico'" in panel
assert "{ id: 'trailer', label: 'Crear tráiler'" in panel
assert "No escribirá ni exigirá una canción" in CATALOG_ES.read_text(encoding="utf-8")
assert "id: 'trailer'" in tabs
assert '"label": "Cinematic trailer"' in catalog_en
assert '"label": "Tráiler cinematográfico"' in catalog_es
assert '"createTrailer": "Create trailer"' in catalog_en
assert '"createTrailer": "Crear tráiler"' in catalog_es
assert "No escribirá ni exigirá una canción" in catalog_es
assert "musicVideoGenerationMode: 'image_guided' as const" in panel
assert "project.projectType === 'trailer' ? 'trailer' : 'productions'" in panel
assert 'if project_type in {"trailer", "quick_video"}' in backend
Expand All @@ -66,10 +73,12 @@ def test_trailer_adapter_enforces_a_story_arc_without_revealing_the_ending():

def test_story_lab_exposes_editable_trailer_controls_and_timed_preview():
source = story_lab_trailer_ui()
panel = PANEL.read_text(encoding="utf-8")
tabs = TABS.read_text(encoding="utf-8")
trailer = source

assert "{ id: 'trailer', label: 'Tráiler'" in panel
assert "id: 'trailer'" in tabs
assert '"trailer": "Trailer"' in CATALOG_EN.read_text(encoding="utf-8")
assert '"trailer": "Tráiler"' in CATALOG_ES.read_text(encoding="utf-8")
assert "Creador de tráileres cinematográficos" in source
assert "TRAILER_ARC.map" in trailer
assert "setTrailerDuration" in trailer
Expand Down Expand Up @@ -108,7 +117,7 @@ def test_trailer_orientation_can_override_the_global_landscape_default_inline():
assert "if (project.provider.useGlobalProfile) return" not in handler
assert "Formato seleccionado" in source
assert "aria-pressed={aspectRatio === option.value}" in source
assert "Formato de vídeo actualizado:" in handler
assert "t('notice.videoFormatUpdated'" in handler


def test_trailer_supports_text_only_direct_video_without_visual_inputs():
Expand Down
12 changes: 6 additions & 6 deletions tests/test_story_montage_clip_history_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
def test_story_montage_exposes_slot_history_and_explicit_remake_action():
timeline = TIMELINE.read_text(encoding="utf-8")

assert "Historial de esta posición" in timeline
assert "En montaje:" in timeline
assert "Rehacer este clip" in timeline
assert "t('timeline.slotHistory')" in timeline
assert "t('timeline.inAssembly'" in timeline
assert "t('timeline.remakeClip')" in timeline
assert "selectPipelineClipVideo" in timeline
assert "directorClipCreatorMetadata" in timeline
assert "writeDirectorClipReplacementTarget" in timeline
Expand All @@ -43,9 +43,9 @@ def test_generated_video_can_be_selected_and_returns_to_story_montage():
main = MAIN.read_text(encoding="utf-8")
client = api_client_source()

assert "Usar en Montaje · clip" in media
assert "t('montage.useInAssembly'" in media
assert "writeDirectorClipReplacementResult" in media
assert "setMediaFilter('stories')" in media
assert "Ajusta sus datos, genera una o varias versiones" in main
assert "Cancelar reemplazo" in main
assert "tActivity('montage.redoingAssemblyClip'" in main
assert "tActivity('montage.cancelReplacement')" in main
assert "/video-selection" in client
5 changes: 4 additions & 1 deletion tests/test_video_editor_replacement_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def test_generated_video_can_replace_only_the_original_timeline_slot():

assert "maestro-video-editor-replacement-target-v1" in handoff
assert "maestro-video-editor-replacement-result-v1" in handoff
assert "Usar en posición" in feed
assert "t('montage.useInPosition'" in feed
assert '"useInPosition": "Usar en posición {{n}}"' in (
ROOT / "ui" / "src" / "i18n" / "locales" / "es" / "activity.json"
).read_text(encoding="utf-8")
assert "writeVideoEditorReplacementResult" in feed
assert "clearVideoEditorReplacementTarget" in main
assert "readVideoEditorReplacementResult" in editor
Expand Down
2 changes: 1 addition & 1 deletion ui/e2e/specs/story-lab-music-productions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test('opens extracted Story Lab Music, Trailer and Productions chrome', async ({
await expect(page.getByRole('heading', { name: 'Comic adaptation' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Film adaptation' })).toBeVisible()

await navigation.getByRole('button', { name: 'Tráiler' }).click()
await navigation.getByRole('button', { name: 'Trailer' }).click()
await expect(page.getByRole('heading', { name: 'Cinematic trailer creator' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Generate complete trailer' })).toBeVisible()

Expand Down
22 changes: 21 additions & 1 deletion ui/scripts/check-i18n-catalogs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const NAMESPACES = ['common', 'navigation', 'settings', 'wizard', 'activity', 'extraInfo', 'storyLab']
const NAMESPACES = ['common', 'navigation', 'settings', 'wizard', 'activity', 'extraInfo', 'storyLab', 'director', 'seriesLab']
const LANGUAGES = ['en', 'es']

function load(language, namespace) {
Expand Down Expand Up @@ -45,6 +45,16 @@ const PILOT_FILES = [
'src/features/workspaceCollections/WorkspaceCollectionsPanel.tsx',
'src/features/workspaces/WorkspacesPanel.tsx',
'src/features/stories/storyLabChrome.tsx',
'src/features/stories/StoryLabNavigation.tsx',
'src/features/stories/storyLabTabs.ts',
'src/features/stories/StoryLabLibraryChrome.tsx',
'src/features/stories/StoryProductionTimeline.tsx',
'src/components/WelcomeModal.tsx',
'src/components/QueueRecoveryDialog.tsx',
'src/features/series/SeriesLabPanel.tsx',
'src/features/series/SeriesReviewPanel.tsx',
'src/features/series/SeriesShotDurationControl.tsx',
'src/features/stories/StoryAssemblyTab.tsx',
'src/features/stories/StoryOverviewTab.tsx',
'src/features/stories/StoryAssetsTab.tsx',
'src/features/stories/StoryAssetsImporter.tsx',
Expand Down Expand Up @@ -107,6 +117,16 @@ const FORBIDDEN = [
'Cinematic trailer creator',
'Generate / refresh Lyria prompt',
'Import custom MP3',
'Montaje de producciones',
'Smart assets',
'Guided · approve stages',
'Preparar historia completa · solo texto',
'Desliza para más secciones',
'Novedades de HocusPocus',
'Entrar al estudio',
'Montaje ordenado',
'Usar en Montaje',
'Hay una cola de generación por recuperar',
]

export function forbiddenLiterals() {
Expand Down
11 changes: 5 additions & 6 deletions ui/src/components/MainContent/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ function PipelinePlaceholder() {
export function MainContent() {
const { t } = useUiTranslation('navigation')
const { t: tCommon } = useUiTranslation('common')
const { t: tActivity } = useUiTranslation('activity')
const outputs = useStore(s => s.filteredOutputs())
const outputsTotal = useStore(s => s.outputsTotal)
const outputsLoading = useStore(s => s.outputsLoading)
Expand Down Expand Up @@ -852,8 +853,7 @@ export function MainContent() {
<div className="flex flex-wrap items-center gap-2 rounded-lg border border-emerald-500/35 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200">
<Film size={14} className="shrink-0" />
<span className="min-w-0 flex-1">
Rehaciendo la posición {replacementTarget.clipIndex + 1} del montaje: <strong>{replacementTarget.originalName}</strong>.
Genera un vídeo nuevo y después pulsa “Usar en posición {replacementTarget.clipIndex + 1}”.
{tActivity('montage.redoingEditorSlot', { n: replacementTarget.clipIndex + 1, name: replacementTarget.originalName })}
</span>
<button
type="button"
Expand All @@ -863,16 +863,15 @@ export function MainContent() {
}}
className="rounded border border-emerald-400/30 px-2 py-1 text-[10px] text-emerald-200 hover:bg-emerald-500/20"
>
Cancelar reemplazo
{tActivity('montage.cancelReplacement')}
</button>
</div>
)}
{directorReplacementTarget && (
<div className="flex flex-wrap items-center gap-2 rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 py-2 text-xs text-violet-100">
<RefreshCw size={14} className="shrink-0" />
<span className="min-w-0 flex-1">
Rehaciendo el clip {directorReplacementTarget.clipIndex + 1} de Montaje.
Ajusta sus datos, genera una o varias versiones y pulsa “Usar en Montaje · clip {directorReplacementTarget.clipIndex + 1}” en la que quieras conservar.
{tActivity('montage.redoingAssemblyClip', { n: directorReplacementTarget.clipIndex + 1 })}
</span>
<button
type="button"
Expand All @@ -882,7 +881,7 @@ export function MainContent() {
}}
className="rounded border border-violet-400/30 px-2 py-1 text-[10px] text-violet-100 hover:bg-violet-500/20"
>
Cancelar reemplazo
{tActivity('montage.cancelReplacement')}
</button>
</div>
)}
Expand Down
8 changes: 4 additions & 4 deletions ui/src/components/MainContent/MediaFeedItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -747,20 +747,20 @@ export function MediaFeedItem({ file, index, isActive, onVisible, onMeasured, st
onClick={() => void handleUseAsDirectorReplacement()}
disabled={selectingForMontage}
className="flex items-center gap-1 rounded-lg border border-violet-500/40 bg-violet-500/10 px-2 py-1.5 text-[10px] font-medium text-violet-200 transition-colors hover:bg-violet-500/20 disabled:opacity-50"
title={`Elegir este vídeo como versión activa del clip ${directorReplacementTarget.clipIndex + 1}; las demás versiones se conservarán en su historial`}
title={t('montage.useInAssembly', { n: directorReplacementTarget.clipIndex + 1 })}
>
{selectingForMontage ? <Loader2 size={13} className="animate-spin" /> : <FolderInput size={13} />}
Usar en Montaje · clip {directorReplacementTarget.clipIndex + 1}
{t('montage.useInAssembly', { n: directorReplacementTarget.clipIndex + 1 })}
</button>
)}
{file.type === 'video' && editorReplacementTarget && (
<button
onClick={handleUseAsEditorReplacement}
className="flex items-center gap-1 rounded-lg border border-emerald-500/35 bg-emerald-500/10 px-2 py-1.5 text-[10px] font-medium text-emerald-300 transition-colors hover:bg-emerald-500/20 hover:text-emerald-200"
title={`Usar este vídeo en la posición ${editorReplacementTarget.clipIndex + 1} del montaje`}
title={t('montage.useInPosition', { n: editorReplacementTarget.clipIndex + 1 })}
>
<FolderInput size={13} />
Usar en posición {editorReplacementTarget.clipIndex + 1}
{t('montage.useInPosition', { n: editorReplacementTarget.clipIndex + 1 })}
</button>
)}
{file.type === 'video' && (
Expand Down
Loading