+ )}
+
+ )
+}
diff --git a/frontend/src/spec/settings.ts b/frontend/src/spec/settings.ts
new file mode 100644
index 0000000..dc597ce
--- /dev/null
+++ b/frontend/src/spec/settings.ts
@@ -0,0 +1,19 @@
+import type { VideoSpec, Waveform } from '@/api/projects'
+
+/**
+ * Project-wide settings as pure functions from one spec to the next (the rules of
+ * `edits.ts`). Each setting is a field of the spec, so a project template can later be
+ * nothing more than a partial spec applied over these same fields.
+ */
+
+/** The wave on (with these settings) or off (`null`). Off removes the block: "present
+ * means on" is the format's rule, and an emptied `chrome` is removed with it. */
+export function setWaveform(spec: VideoSpec, waveform: Waveform | null): VideoSpec {
+ const chrome = { ...(spec.chrome ?? {}) }
+ if (waveform === null) delete chrome.waveform
+ else chrome.waveform = waveform
+ if (Object.keys(chrome).length > 0) return { ...spec, chrome }
+ const rest = { ...spec }
+ delete rest.chrome
+ return rest
+}
diff --git a/frontend/src/spec/waveform.ts b/frontend/src/spec/waveform.ts
new file mode 100644
index 0000000..dd1bbb0
--- /dev/null
+++ b/frontend/src/spec/waveform.ts
@@ -0,0 +1,26 @@
+import type { WaveMode, WavePosition, Waveform } from '@/api/projects'
+
+// Notes' names for the wave styles, so the same wave is called the same in both.
+export const WAVE_MODES: { value: WaveMode; label: string }[] = [
+ { value: 'line', label: 'Line' },
+ { value: 'cline', label: 'Centred line' },
+ { value: 'p2p', label: 'Peak to peak' },
+ { value: 'point', label: 'Points' },
+]
+
+export const WAVE_POSITIONS: { value: WavePosition; label: string }[] = [
+ { value: 'top', label: 'Top' },
+ { value: 'center', label: 'Centre' },
+ { value: 'bottom', label: 'Bottom' },
+]
+
+/** The same defaults as the waveform in Gecko Notes' video dialog. Written out in full
+ * so the controls show what is drawn rather than a blank that means "the default". */
+export const DEFAULT_WAVEFORM: Required = {
+ mode: 'line',
+ color: '#00ff41',
+ opacity: 0.7,
+ position: 'bottom',
+ height_pct: 22,
+ scrim: 0.45,
+}
diff --git a/frontend/src/views/ProjectView.tsx b/frontend/src/views/ProjectView.tsx
index be85607..40d83cd 100644
--- a/frontend/src/views/ProjectView.tsx
+++ b/frontend/src/views/ProjectView.tsx
@@ -11,6 +11,7 @@ import SoundtrackPanel from '@/components/SoundtrackPanel'
import IssueList from '@/components/IssueList'
import JsonPanel from '@/components/JsonPanel'
import ProjectDefaults from '@/components/ProjectDefaults'
+import WaveformSettings from '@/components/WaveformSettings'
import SceneList from '@/components/SceneList'
import { RUNNING } from '@/api/generations'
import { gamIdsIn, placeHit, resolveSearch, unresolvedSearches } from '@/spec/footage'
@@ -18,7 +19,7 @@ import { canGenerate, latestByKey, unmadeGenerates } from '@/spec/generation'
import { useGenerationStore } from '@/stores/generations'
import { useProjectStore } from '@/stores/projects'
-type Tab = 'scenes' | 'soundtrack' | 'render' | 'json' | 'history'
+type Tab = 'scenes' | 'soundtrack' | 'settings' | 'render' | 'json' | 'history'
/** What the footage search is open for: one scene, or a placeholder to resolve. */
type Finding = { kind: 'scene'; index: number } | { kind: 'resolve'; key: string }
@@ -26,6 +27,7 @@ type Finding = { kind: 'scene'; index: number } | { kind: 'resolve'; key: string
const TABS: { id: Tab; label: string }[] = [
{ id: 'scenes', label: 'Scenes' },
{ id: 'soundtrack', label: 'Soundtrack' },
+ { id: 'settings', label: 'Settings' },
{ id: 'render', label: 'Render' },
{ id: 'json', label: 'JSON' },
{ id: 'history', label: 'History' },
@@ -283,7 +285,6 @@ export default function ProjectView() {
Could not show pictures from the asset library: {gamError}
)}
- {tab === 'scenes' && }
{tab === 'scenes' && (
)}
+ {tab === 'settings' && (
+ // Everything that applies to the whole video rather than one scene. The home
+ // for the rest of the project-wide settings, and for templates over them.
+
+
+
+
+ )}
{tab === 'render' && (
From b01f86b0f3c96bc972f7b4add1adc60faecc85b4 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 24 Oct 2026 04:53:42 +0000
Subject: [PATCH 2/2] Add Output and Voice sections to the Settings tab
Output: shape, size, quality, frame rate, subtitles, chapter marks and cover picture.
Voice: a named voice, speaking speed and the three pauses. Both write only what
differs from the spec's own defaults, and remove a section left empty.
The spec also has `voice.provider` and `voice.expressivity`, which the renderer does
not read yet (it warns about expressivity), so there are no controls for them: a
control that does nothing is worse than none.
Co-Authored-By: Claude Sonnet 5.5
Claude-Session: https://claude.ai/code/session_01XHkHXgxVVH3dn6EAVL9rbN
---
frontend/src/api/projects.ts | 28 ++++-
frontend/src/components/OutputSettings.tsx | 117 ++++++++++++++++++
.../components/OutputVoiceSettings.test.tsx | 70 +++++++++++
frontend/src/components/VoiceSettings.tsx | 81 ++++++++++++
frontend/src/spec/settings.test.ts | 37 ++++++
frontend/src/spec/settings.ts | 72 ++++++++++-
frontend/src/views/ProjectView.tsx | 4 +
7 files changed, 407 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/components/OutputSettings.tsx
create mode 100644 frontend/src/components/OutputVoiceSettings.test.tsx
create mode 100644 frontend/src/components/VoiceSettings.tsx
create mode 100644 frontend/src/spec/settings.test.ts
diff --git a/frontend/src/api/projects.ts b/frontend/src/api/projects.ts
index 7a1ed21..528ada4 100644
--- a/frontend/src/api/projects.ts
+++ b/frontend/src/api/projects.ts
@@ -186,6 +186,31 @@ export interface Segment {
[key: string]: unknown
}
+export type Aspect = '16:9' | '9:16' | '1:1'
+export type Resolution = '720p' | '1080p' | '4k'
+export type Quality = 'preview' | 'standard' | 'high'
+export type SubtitleMode = 'off' | 'sidecar' | 'soft' | 'burn'
+
+/** What the finished file is: its shape, size and extras. */
+export interface Output {
+ aspect?: Aspect
+ resolution?: Resolution
+ quality?: Quality
+ fps?: number
+ subtitles?: SubtitleMode
+ chapters?: boolean
+ thumbnail?: boolean
+ [key: string]: unknown
+}
+
+/** The narrator, for every scene that doesn't give its own. */
+export interface Voice {
+ voice?: string
+ speed?: number
+ pauses?: { paragraph_ms?: number; heading_ms?: number; segment_end_ms?: number }
+ [key: string]: unknown
+}
+
export interface VideoSpec {
spec_version?: string
id?: string
@@ -195,7 +220,8 @@ export interface VideoSpec {
segments: Segment[]
audio_tracks?: AudioTrack[]
credits?: Credits | null
- output?: { aspect?: '16:9' | '9:16' | '1:1'; [key: string]: unknown }
+ output?: Output
+ voice?: Voice
/** Drawn over every scene that doesn't opt out. A block present means on. */
chrome?: { waveform?: Waveform; [key: string]: unknown }
style?: {
diff --git a/frontend/src/components/OutputSettings.tsx b/frontend/src/components/OutputSettings.tsx
new file mode 100644
index 0000000..efc0779
--- /dev/null
+++ b/frontend/src/components/OutputSettings.tsx
@@ -0,0 +1,117 @@
+import type { Aspect, Quality, Resolution, SubtitleMode, VideoSpec } from '@/api/projects'
+import { OUTPUT_DEFAULTS, setOutput } from '@/spec/settings'
+
+interface Props {
+ spec: VideoSpec
+ onChange: (next: VideoSpec) => void
+}
+
+const ASPECTS: { value: Aspect; label: string }[] = [
+ { value: '16:9', label: 'Widescreen 16:9' },
+ { value: '9:16', label: 'Vertical 9:16' },
+ { value: '1:1', label: 'Square 1:1' },
+]
+
+const RESOLUTIONS: { value: Resolution; label: string }[] = [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '4k', label: '4K' },
+]
+
+const QUALITIES: { value: Quality; label: string }[] = [
+ { value: 'preview', label: 'Fast, larger file' },
+ { value: 'standard', label: 'Standard' },
+ { value: 'high', label: 'High, slower' },
+]
+
+const SUBTITLES: { value: SubtitleMode; label: string }[] = [
+ { value: 'off', label: 'None' },
+ { value: 'sidecar', label: 'A separate .srt file' },
+ { value: 'soft', label: 'Inside the video, can be switched off' },
+ { value: 'burn', label: 'Drawn onto the picture' },
+]
+
+/**
+ * The finished file: its shape, size, frame rate and extras. Aspect is also what the
+ * scene editor frames a camera move against, so changing it changes those previews.
+ */
+export default function OutputSettings({ spec, onChange }: Props) {
+ const o = { ...OUTPUT_DEFAULTS, ...(spec.output ?? {}) }
+ const select = (
+ label: string,
+ value: T,
+ options: { value: T; label: string }[],
+ set: (v: T) => void
+ ) => (
+
+ )
+
+ return (
+
+