Adding gnommoeditor in the current version

This commit is contained in:
2026-04-11 09:24:21 +02:00
commit 53d310da82
237 changed files with 64938 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
exports.up = (pgm) => {
pgm.createTable('videos', {
id: 'id',
gnommo_id: { type: 'varchar(50)', notNull: true, unique: true },
course_code: { type: 'varchar(100)' },
title: { type: 'varchar(500)', notNull: true },
description: { type: 'text' },
hook: { type: 'text' },
status: { type: 'varchar(50)' },
resolution_width: { type: 'integer' },
resolution_height: { type: 'integer' },
fps: { type: 'integer' },
default_slide_type: { type: 'varchar(50)' },
background: { type: 'varchar(500)' },
platform_targets: { type: 'jsonb' },
duration_seconds: { type: 'numeric' },
youtube_url: { type: 'varchar(500)' },
raw_project_json: { type: 'jsonb' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.createIndex('videos', 'gnommo_id', { name: 'idx_videos_gnommo_id' });
};
exports.down = (pgm) => {
pgm.dropTable('videos');
};
+30
View File
@@ -0,0 +1,30 @@
exports.up = (pgm) => {
pgm.createTable('slides', {
id: 'id',
video_id: {
type: 'integer',
notNull: true,
references: 'videos',
onDelete: 'CASCADE',
},
gnommo_slide_id: { type: 'varchar(20)', notNull: true },
slide_order: { type: 'integer', notNull: true },
image_filename: { type: 'varchar(500)' },
display_mode: { type: 'varchar(50)' },
component_key: { type: 'varchar(100)' },
props: { type: 'jsonb' },
presenter_notes: { type: 'text' },
start_time_sec: { type: 'numeric' },
end_time_sec: { type: 'numeric' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.addConstraint('slides', 'slides_video_id_gnommo_slide_id_unique', 'UNIQUE (video_id, gnommo_slide_id)');
pgm.createIndex('slides', 'video_id', { name: 'idx_slides_video_id' });
pgm.createIndex('slides', ['video_id', 'slide_order'], { name: 'idx_slides_video_order' });
};
exports.down = (pgm) => {
pgm.dropTable('slides');
};
+17
View File
@@ -0,0 +1,17 @@
// Extend videos table with fields from project.json not captured in 001
exports.up = (pgm) => {
pgm.addColumns('videos', {
output_video: { type: 'varchar(200)' },
keynote_file: { type: 'varchar(500)' },
parent_gnommo_id: { type: 'varchar(50)' }, // gnommo_id of parent (for shorts)
outro: { type: 'jsonb' }, // ["outrovideo1"]
shorts: { type: 'jsonb' }, // ["short_pixelated_universe", ...]
manuscript_path: { type: 'varchar(500)' }, // "manuscript.txt"
});
};
exports.down = (pgm) => {
pgm.dropColumns('videos', [
'output_video', 'keynote_file', 'parent_gnommo_id', 'outro', 'shorts', 'manuscript_path',
]);
};
+19
View File
@@ -0,0 +1,19 @@
// Cutout definitions from project.json "cutouts" section.
// A cutout is a named viewport region (talkinghead, square, fullscreen, fullscreen2).
// x/y/width/height are stored as strings because gnommo uses both "%" and raw values.
exports.up = (pgm) => {
pgm.createTable('cutouts', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
name: { type: 'varchar(50)', notNull: true }, // talkinghead | square | fullscreen | fullscreen2
x: { type: 'varchar(20)' }, // e.g. "-30%" or "0%"
y: { type: 'varchar(20)' },
width: { type: 'varchar(20)' }, // optional — height alone can define aspect
height: { type: 'varchar(20)' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.addConstraint('cutouts', 'cutouts_project_name_unique', 'UNIQUE (project_id, name)');
};
exports.down = (pgm) => { pgm.dropTable('cutouts'); };
@@ -0,0 +1,20 @@
// Per-project processing filter chains from project.json "default_filters".
// Each row is one filter step within a named target pipeline (audioonly, talkinghead, etc.).
// Filter-specific parameters (LUFS targets, chroma key colours, mask boundaries) live in params (jsonb).
exports.up = (pgm) => {
pgm.createTable('filter_chains', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
target: { type: 'varchar(50)', notNull: true }, // audioonly | talkinghead
sort_order: { type: 'integer', notNull: true },
filter_type: { type: 'varchar(50)', notNull: true }, // audio_normalize | color_grade | gnommokey | mask
params: { type: 'jsonb' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.createIndex('filter_chains', ['project_id', 'target', 'sort_order'], {
name: 'idx_filter_chains_project_target_order',
});
};
exports.down = (pgm) => { pgm.dropTable('filter_chains'); };
@@ -0,0 +1,32 @@
// Per-project video asset library from media/videos/videos.json.
// Includes both local clips and references to shared assets (is_shared=true).
// minio_object_key is null today; populated when assets migrate to MinIO.
exports.up = (pgm) => {
pgm.createTable('video_assets', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
asset_key: { type: 'varchar(200)', notNull: true }, // key in videos.json
description: { type: 'text' },
source_file: { type: 'varchar(500)' }, // original local path
output_file: { type: 'varchar(500)' }, // rendered output path
minio_object_key: { type: 'varchar(500)' }, // future MinIO key
is_shared: { type: 'boolean', notNull: true, default: false },
cutout_name: { type: 'varchar(50)' }, // talkinghead | square | fullscreen
duration_seconds: { type: 'numeric' },
has_audio: { type: 'boolean', notNull: true, default: false },
volume: { type: 'numeric', default: 1.0 },
always_visible: { type: 'boolean', notNull: true, default: false },
pause_narration_seconds: { type: 'numeric' },
skip_seconds: { type: 'numeric' },
take_seconds: { type: 'numeric' },
filters: { type: 'jsonb' }, // per-asset filter overrides
raw_json: { type: 'jsonb' }, // full original entry
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.addConstraint('video_assets', 'video_assets_project_key_unique', 'UNIQUE (project_id, asset_key)');
pgm.createIndex('video_assets', 'project_id', { name: 'idx_video_assets_project_id' });
pgm.createIndex('video_assets', 'is_shared', { name: 'idx_video_assets_is_shared' });
};
exports.down = (pgm) => { pgm.dropTable('video_assets'); };
@@ -0,0 +1,25 @@
// Narration recording segments from media/narration/narration.json.
// Each segment is a recorded take with skip/take edit points.
// minio_object_key is null today; populated when recordings migrate to MinIO.
exports.up = (pgm) => {
pgm.createTable('narration_segments', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
segment_key: { type: 'varchar(50)', notNull: true }, // Segment001, Segment002 ...
source_file: { type: 'varchar(500)' },
minio_object_key: { type: 'varchar(500)' },
use_audio_channels: { type: 'varchar(20)', default: 'auto' },
defer_loudnorm: { type: 'boolean', notNull: true, default: false },
skip_seconds: { type: 'numeric' },
take_seconds: { type: 'numeric' },
sort_order: { type: 'integer', notNull: true, default: 0 },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.addConstraint('narration_segments', 'narration_segments_project_key_unique',
'UNIQUE (project_id, segment_key)');
pgm.createIndex('narration_segments', ['project_id', 'sort_order'],
{ name: 'idx_narration_segments_order' });
};
exports.down = (pgm) => { pgm.dropTable('narration_segments'); };
@@ -0,0 +1,22 @@
// Background audio tracks from media/audio/audio.json.
// minio_object_key is null today; populated when audio files migrate to MinIO.
exports.up = (pgm) => {
pgm.createTable('audio_tracks', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
track_key: { type: 'varchar(100)', notNull: true }, // cosmicpad, etc.
source_file: { type: 'varchar(500)' },
minio_object_key: { type: 'varchar(500)' },
volume: { type: 'numeric', notNull: true, default: 1.0 },
loop: { type: 'boolean', notNull: true, default: false },
overlap: { type: 'varchar(20)' }, // "15s"
ignore_pauses: { type: 'boolean', notNull: true, default: false },
duration_seconds: { type: 'numeric' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.addConstraint('audio_tracks', 'audio_tracks_project_key_unique',
'UNIQUE (project_id, track_key)');
};
exports.down = (pgm) => { pgm.dropTable('audio_tracks'); };
@@ -0,0 +1,23 @@
// Global shared asset library from shared_assets/ directory (shared_assets/videos.json,
// plus square/, landscape/, pexels/, models/ subfolders).
// This is the future MinIO object catalogue — minio_object_key is null until migration.
exports.up = (pgm) => {
pgm.createTable('shared_assets', {
id: 'id',
asset_key: { type: 'varchar(200)', notNull: true, unique: true },
asset_type: { type: 'varchar(20)', notNull: true }, // video | image | audio | model
subfolder: { type: 'varchar(100)' }, // pexels | square | landscape | models
filename: { type: 'varchar(500)', notNull: true },
minio_object_key: { type: 'varchar(500)' },
description: { type: 'text' },
duration_seconds: { type: 'numeric' },
has_audio: { type: 'boolean', notNull: true, default: false },
file_size_bytes: { type: 'bigint' },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.createIndex('shared_assets', 'asset_type', { name: 'idx_shared_assets_type' });
pgm.createIndex('shared_assets', 'subfolder', { name: 'idx_shared_assets_subfolder' });
};
exports.down = (pgm) => { pgm.dropTable('shared_assets'); };
@@ -0,0 +1,13 @@
exports.up = (pgm) => {
pgm.createTable('citations', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
reference: { type: 'text', notNull: true },
context: { type: 'text' },
sort_order: { type: 'integer', notNull: true, default: 0 },
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.createIndex('citations', 'project_id', { name: 'idx_citations_project_id' });
};
exports.down = (pgm) => { pgm.dropTable('citations'); };
@@ -0,0 +1,15 @@
// Sync metadata from .gnommo_sync.json — tracks push/pull history with gnommo tool.
exports.up = (pgm) => {
pgm.createTable('project_sync', {
id: 'id',
project_id: { type: 'integer', notNull: true, unique: true,
references: 'videos', onDelete: 'CASCADE' },
last_pulled_at: { type: 'timestamptz' },
server_updated_at: { type: 'timestamptz' },
last_pushed_at: { type: 'timestamptz' },
last_handoff_at: { type: 'timestamptz' },
video_version: { type: 'integer', notNull: true, default: 0 },
});
};
exports.down = (pgm) => { pgm.dropTable('project_sync'); };
+14
View File
@@ -0,0 +1,14 @@
// Add richer slide spec fields used in detailed per-slide JSON (e.g. video5 spec/ directory).
exports.up = (pgm) => {
pgm.addColumns('slides', {
illustration_type: { type: 'varchar(50)' }, // glitch | transition | etc.
slide_target: { type: 'varchar(50)' }, // image | svg
symbols: { type: 'jsonb' }, // ["noise cloud", ...]
caption: { type: 'text' },
prompt: { type: 'text' }, // AI illustration prompt
});
};
exports.down = (pgm) => {
pgm.dropColumns('slides', ['illustration_type', 'slide_target', 'symbols', 'caption', 'prompt']);
};
@@ -0,0 +1,16 @@
// Word-level speech-to-text transcripts from narration_combined.transcript.json.
// Stored as a jsonb array [{word, start, end}] per source to avoid millions of tiny rows.
exports.up = (pgm) => {
pgm.createTable('transcripts', {
id: 'id',
project_id: { type: 'integer', notNull: true, references: 'videos', onDelete: 'CASCADE' },
source_name: { type: 'varchar(100)', notNull: true }, // narration_combined | talkinghead
words: { type: 'jsonb', notNull: true }, // [{word, start, end}, ...]
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') },
});
pgm.addConstraint('transcripts', 'transcripts_project_source_unique',
'UNIQUE (project_id, source_name)');
};
exports.down = (pgm) => { pgm.dropTable('transcripts'); };
@@ -0,0 +1,13 @@
// Add dimension and MIME metadata captured at upload time
exports.up = (pgm) => {
pgm.addColumns('shared_assets', {
width: { type: 'integer' },
height: { type: 'integer' },
mime_type: { type: 'varchar(100)' },
original_filename: { type: 'varchar(500)' },
});
};
exports.down = (pgm) => {
pgm.dropColumns('shared_assets', ['width', 'height', 'mime_type', 'original_filename']);
};
+66
View File
@@ -0,0 +1,66 @@
// Convert videos.id and slides.id (plus every FK that references them) from
// serial integers to UUID (gen_random_uuid).
//
// Safe for dev: truncates all video data first. Re-run the push script after
// migrating to re-ingest videos. shared_assets (asset library) is unchanged.
//
// This is a one-way migration. There is no meaningful down path.
const CHILD_TABLES = [
// table FK constraint name UNIQUE constraint name UNIQUE expression
['narration_segments', 'narration_segments_project_id_fkey', 'narration_segments_project_key_unique', '(project_id, segment_key)'],
['cutouts', 'cutouts_project_id_fkey', 'cutouts_project_name_unique', '(project_id, name)'],
['filter_chains', 'filter_chains_project_id_fkey', null, null],
['video_assets', 'video_assets_project_id_fkey', 'video_assets_project_key_unique', '(project_id, asset_key)'],
['audio_tracks', 'audio_tracks_project_id_fkey', 'audio_tracks_project_key_unique', '(project_id, track_key)'],
['citations', 'citations_project_id_fkey', null, null],
['project_sync', 'project_sync_project_id_fkey', 'project_sync_project_id_key', '(project_id)'],
['transcripts', 'transcripts_project_id_fkey', 'transcripts_project_source_unique', '(project_id, source_name)'],
];
exports.up = (pgm) => {
// ── Clear all video data ─────────────────────────────────────────────────
pgm.sql('TRUNCATE videos CASCADE');
// ── 1. Drop all FKs pointing at videos.id BEFORE touching the PK ────────
// slides
pgm.sql('ALTER TABLE slides DROP CONSTRAINT IF EXISTS slides_video_id_fkey');
pgm.sql('ALTER TABLE slides DROP CONSTRAINT IF EXISTS slides_video_id_gnommo_slide_id_unique');
// all other child tables
for (const [table, fkName, uniqueName] of CHILD_TABLES) {
pgm.sql(`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fkName}`);
if (uniqueName) pgm.sql(`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${uniqueName}`);
}
// ── 2. videos.id: serial → uuid ─────────────────────────────────────────
pgm.sql('ALTER TABLE videos ALTER COLUMN id DROP DEFAULT');
pgm.sql('ALTER TABLE videos DROP CONSTRAINT videos_pkey');
pgm.sql('ALTER TABLE videos ALTER COLUMN id TYPE uuid USING gen_random_uuid()');
pgm.sql('ALTER TABLE videos ALTER COLUMN id SET DEFAULT gen_random_uuid()');
pgm.sql('ALTER TABLE videos ADD PRIMARY KEY (id)');
// ── 3. slides: video_id FK column, then id PK ───────────────────────────
pgm.sql('ALTER TABLE slides ALTER COLUMN video_id TYPE uuid USING gen_random_uuid()');
pgm.sql('ALTER TABLE slides ADD CONSTRAINT slides_video_id_gnommo_slide_id_unique UNIQUE (video_id, gnommo_slide_id)');
pgm.sql('ALTER TABLE slides ADD CONSTRAINT slides_video_id_fkey FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE');
pgm.sql('ALTER TABLE slides ALTER COLUMN id DROP DEFAULT');
pgm.sql('ALTER TABLE slides DROP CONSTRAINT slides_pkey');
pgm.sql('ALTER TABLE slides ALTER COLUMN id TYPE uuid USING gen_random_uuid()');
pgm.sql('ALTER TABLE slides ALTER COLUMN id SET DEFAULT gen_random_uuid()');
pgm.sql('ALTER TABLE slides ADD PRIMARY KEY (id)');
// ── 4. All other child tables: project_id integer → uuid ────────────────
for (const [table, fkName, uniqueName, uniqueExpr] of CHILD_TABLES) {
pgm.sql(`ALTER TABLE ${table} ALTER COLUMN project_id TYPE uuid USING gen_random_uuid()`);
if (uniqueName && uniqueExpr) {
pgm.sql(`ALTER TABLE ${table} ADD CONSTRAINT ${uniqueName} UNIQUE ${uniqueExpr}`);
}
pgm.sql(`ALTER TABLE ${table} ADD CONSTRAINT ${fkName} FOREIGN KEY (project_id) REFERENCES videos(id) ON DELETE CASCADE`);
}
};
exports.down = () => {
// Intentionally empty — reversing uuid → serial would require re-sequencing
// all IDs and is not worth implementing for a dev-only migration.
};
@@ -0,0 +1,20 @@
// Allows an asset to declare that it is a processed derivative of another asset.
// Example lineage: raw .mov → chroma-keyed .mp4 (parent_id → raw asset)
// A raw asset has parent_id = NULL.
// Multiple processed versions can share the same parent_id.
exports.up = (pgm) => {
pgm.addColumn('shared_assets', {
parent_id: {
type: 'integer',
references: 'shared_assets',
onDelete: 'SET NULL',
},
});
pgm.createIndex('shared_assets', 'parent_id', { name: 'idx_shared_assets_parent' });
};
exports.down = (pgm) => {
pgm.dropIndex('shared_assets', 'parent_id', { name: 'idx_shared_assets_parent' });
pgm.dropColumn('shared_assets', 'parent_id');
};
@@ -0,0 +1,34 @@
// Two additions:
//
// 1. asset_id — direct FK to shared_assets, preferred over minio_object_key
// for segments created through the editor UI. Gnommo-ingested segments
// continue to use minio_object_key; asset_id is NULL for those.
//
// 2. segment_type — 'raw' for original recordings, 'final' for the
// processed/transcoded versions that the player actually consumes.
// The GnommoPlayer export uses the 'final' list; 'raw' is for reference
// and source-of-truth tracking.
exports.up = (pgm) => {
pgm.addColumns('narration_segments', {
asset_id: {
type: 'integer',
references: 'shared_assets',
onDelete: 'SET NULL',
},
segment_type: {
type: 'varchar(10)',
notNull: true,
default: "'raw'",
},
});
pgm.createIndex('narration_segments', ['project_id', 'segment_type', 'sort_order'], {
name: 'idx_narration_segments_type_order',
});
};
exports.down = (pgm) => {
pgm.dropIndex('narration_segments', null, { name: 'idx_narration_segments_type_order' });
pgm.dropColumns('narration_segments', ['asset_id', 'segment_type']);
};
@@ -0,0 +1,22 @@
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE slide_candidates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
slide_id uuid NOT NULL REFERENCES slides ON DELETE CASCADE,
component_key varchar(100) NOT NULL,
display_mode varchar(50),
props jsonb,
score numeric NOT NULL DEFAULT 0,
proposed_by varchar(200) NOT NULL,
proposed_by_name varchar(200),
created_at timestamptz DEFAULT NOW(),
updated_at timestamptz DEFAULT NOW(),
UNIQUE(slide_id, proposed_by)
)
`);
pgm.sql('CREATE INDEX idx_slide_candidates_slide_id ON slide_candidates(slide_id)');
};
exports.down = (pgm) => {
pgm.dropTable('slide_candidates');
};
@@ -0,0 +1,15 @@
// Attribution metadata for the shared asset library.
// Covers provenance (where it came from), legal (license, rights holder),
// and internal tracking (who uploaded it).
exports.up = (pgm) => {
pgm.addColumns('shared_assets', {
license: { type: 'varchar(200)' }, // e.g. "Pexels License", "CC BY 4.0", "CC0 1.0"
copyright_holder: { type: 'varchar(500)' }, // photographer / creator name
source_url: { type: 'varchar(1000)' }, // page URL where asset was obtained
uploader: { type: 'varchar(200)' }, // who added it to this system
});
};
exports.down = (pgm) => {
pgm.dropColumns('shared_assets', ['license', 'copyright_holder', 'source_url', 'uploader']);
};