cineprompt 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * CinePrompt prompt text builder.
3
+ * Mirrors the frontend's prompt assembly logic: section ordering,
4
+ * field merging, and sentence construction.
5
+ */
6
+
7
+ // Natural language join: ["a", "b", "c"] → "a, b and c"
8
+ function nlJoin(arr) {
9
+ if (!Array.isArray(arr)) return arr;
10
+ if (arr.length <= 1) return arr[0] || '';
11
+ return arr.slice(0, -1).join(', ') + ' and ' + arr[arr.length - 1];
12
+ }
13
+
14
+ // Section ordering (Universal model default)
15
+ const SECTION_ORDER = [
16
+ { section: 'STYLE', fields: ['media_type', 'commercial_type', 'documentary_style', 'animation_style', 'music_video_style', 'social_media_style', 'genre', 'tone', 'format'] },
17
+ { section: 'SUBJECT', fields: ['char_label', 'age_range', 'build', 'hair_style', 'hair_color', 'subject_description', 'wardrobe', 'expression', 'body_language', 'framing', 'creature_category', 'creature_label', 'creature_size', 'creature_body', 'creature_skin', 'creature_description', 'creature_expression', 'creature_framing', 'obj_description', 'obj_material', 'obj_condition', 'obj_scale', 'prod_description', 'prod_material', 'prod_staging', 'prod_condition', 'food_description', 'food_state', 'food_presentation', 'food_texture', 'cloth_description', 'cloth_fabric', 'cloth_presentation', 'cloth_fit', 'art_description', 'art_medium', 'art_setting', 'art_condition', 'botan_description', 'botan_type', 'botan_stage', 'botan_detail', 'veh_type', 'veh_description', 'veh_era', 'veh_condition', 'land_season', 'land_scale', 'abs_description', 'abs_quality', 'abs_movement'] },
18
+ { section: 'ACTIONS', fields: ['movement_type', 'pacing', 'interaction_type', 'action_primary', 'beat_1', 'beat_2', 'beat_3'] },
19
+ { section: 'ENVIRONMENT', fields: ['setting', 'isolation', 'location_type', 'abstract_environment', 'custom_location', 'location', 'env_time', 'weather', 'props', 'env_fg', 'env_mg', 'env_bg'] },
20
+ { section: 'CINEMATOGRAPHY', fields: ['shot_type', 'movement', 'camera_body', 'focal_length', 'lens_brand', 'lens_filter', 'dof', 'lighting_style', 'lighting_type', 'key_light', 'fill_light'] },
21
+ { section: 'PALETTE', fields: ['color_science', 'film_stock', 'color_grade', 'palette_colors', 'skin_tones'] },
22
+ { section: 'SOUND', fields: ['sound_mode', 'voiceover_text', 'sfx_environment', 'sfx_interior', 'sfx_mechanical', 'sfx_dramatic', 'ambient', 'music_genre', 'music_mood', 'music'] }
23
+ ];
24
+
25
+ // Media type subcategory fields
26
+ const MEDIA_SUBCAT_FIELDS = {
27
+ 'commercial': 'commercial_type', 'cinematic': 'genre', 'documentary': 'documentary_style',
28
+ 'animation': 'animation_style', 'music video': 'music_video_style', 'social media': 'social_media_style'
29
+ };
30
+ const MEDIA_ABSORBED = new Set(['media_type', 'commercial_type', 'documentary_style', 'animation_style', 'music_video_style', 'social_media_style', 'genre']);
31
+
32
+ // Merge rules for field pairs
33
+ function buildMergeRules(fields) {
34
+ return {
35
+ 'shot_type': { mergeWith: 'movement', fn: (a, b) => {
36
+ if (a && b) return b === 'static' ? `${a}, locked-off static camera` : `${a} with ${b} camera movement`;
37
+ if (b) return b === 'static' ? 'locked-off static camera' : `${b} camera movement`;
38
+ return a;
39
+ }},
40
+ 'setting': { mergeWith: 'location_type', fn: (s, lt) => {
41
+ const custom = fields.custom_location || '';
42
+ let loc = lt && custom ? `${lt}, ${custom}` : (lt || custom || '');
43
+ if (s && loc) return `${s}, ${loc}`;
44
+ return s || loc;
45
+ }},
46
+ 'focal_length': { mergeWith: 'lens_brand', fn: (fl, b) => {
47
+ if (fl && b) return `${fl.replace(/ lens$/, '')} ${b}`;
48
+ return fl || b;
49
+ }},
50
+ 'lighting_style': { mergeWith: 'lighting_type', fn: (s, t) => {
51
+ if (s && t) return `${s.replace(/ light$/, '').replace(/ lighting$/, '')} ${t}`;
52
+ return s || t;
53
+ }},
54
+ 'env_time': { mergeWith: 'weather', fn: (t, w) => {
55
+ if (t && w) return `${t}, ${w}`;
56
+ return t || w;
57
+ }},
58
+ 'key_light': { mergeWith: 'fill_light', fn: (k, f) => {
59
+ if (k && f) return `${k}, ${f}`;
60
+ return k || f;
61
+ }},
62
+ 'camera_body': { mergeWith: 'color_science', fn: (cam, cs) => {
63
+ if (cam && cs) {
64
+ let profileName = cs.split(' flat log')[0].split(' flat ')[0];
65
+ const brands = ['ARRI', 'Sony', 'RED', 'Canon', 'Panasonic', 'Blackmagic'];
66
+ for (const brand of brands) {
67
+ if (cam.includes(brand) && profileName.startsWith(brand + ' ')) {
68
+ profileName = profileName.slice(brand.length + 1);
69
+ break;
70
+ }
71
+ }
72
+ return `${cam} in ${profileName}, flat log footage, ungraded`;
73
+ }
74
+ return cam || cs;
75
+ }},
76
+ 'film_stock': { mergeWith: 'color_grade', fn: (s, g) => {
77
+ if (s && g) return `${s}, ${g}`;
78
+ return s || g;
79
+ }},
80
+ 'hair_style': { mergeWith: 'hair_color', fn: (s, c) => {
81
+ if (s && c) return `${s.replace(/ hair$/, '')} ${c.replace(/ hair$/, '')} hair`;
82
+ return s || c;
83
+ }},
84
+ 'expression': { mergeWith: 'body_language', fn: (e, b) => {
85
+ if (e && b) return `${e}, ${b}`;
86
+ return e || b;
87
+ }},
88
+ 'char_label': { mergeWith: 'age_range', fn: (l, a) => {
89
+ if (l && a) return a.startsWith('in their') ? `${l} ${a}` : `${l}, ${a}`;
90
+ return l || a;
91
+ }},
92
+ 'creature_category': { mergeWith: 'creature_label', fn: (c, l) => {
93
+ if (c && l) return `${c}, ${l}`;
94
+ return c || l;
95
+ }},
96
+ 'music_genre': { mergeWith: 'music_mood', fn: (g, m) => {
97
+ if (g && m) return `${m.split(',')[0].trim()} ${g}`;
98
+ return g || m;
99
+ }},
100
+ 'sound_mode': { mergeWith: 'voiceover_text', fn: (m, t) => {
101
+ if (m && t) {
102
+ let vo = t.trim();
103
+ if (!vo.startsWith('"') && !vo.startsWith('\u201c')) vo = `"${vo}"`;
104
+ return `${m}: ${vo}`;
105
+ }
106
+ return m || t;
107
+ }}
108
+ };
109
+ }
110
+
111
+ export function buildPromptText(state) {
112
+ const fields = state.fields || {};
113
+ const mergeRules = buildMergeRules(fields);
114
+
115
+ const skipFields = new Set();
116
+ for (const [, rule] of Object.entries(mergeRules)) {
117
+ if (rule.mergeWith) skipFields.add(rule.mergeWith);
118
+ }
119
+ skipFields.add('custom_location');
120
+
121
+ // Media type smart merge
122
+ let mediaTypeMerged = null;
123
+ if (fields.media_type) {
124
+ const types = Array.isArray(fields.media_type) ? fields.media_type : [fields.media_type];
125
+ const parts = [];
126
+ for (const mt of types) {
127
+ const subcatField = MEDIA_SUBCAT_FIELDS[mt];
128
+ const subcatVal = subcatField ? fields[subcatField] : null;
129
+ if (subcatVal) {
130
+ if (mt === 'cinematic' && subcatVal) {
131
+ const genreArr = Array.isArray(subcatVal) ? subcatVal : [subcatVal];
132
+ parts.push(`cinematic ${nlJoin(genreArr)}`);
133
+ } else if (Array.isArray(subcatVal)) {
134
+ parts.push(nlJoin(subcatVal));
135
+ } else {
136
+ parts.push(subcatVal);
137
+ }
138
+ } else {
139
+ parts.push(mt);
140
+ }
141
+ }
142
+ mediaTypeMerged = parts.join(' ');
143
+ }
144
+
145
+ // Build ordered values
146
+ const allValues = [];
147
+ for (const { section, fields: sectionFields } of SECTION_ORDER) {
148
+ for (const field of sectionFields) {
149
+ if (MEDIA_ABSORBED.has(field)) {
150
+ if (field === 'media_type' && mediaTypeMerged) {
151
+ allValues.push({ text: mediaTypeMerged, section, field });
152
+ }
153
+ continue;
154
+ }
155
+ if (skipFields.has(field)) continue;
156
+ if (mergeRules[field]) {
157
+ const partner = mergeRules[field].mergeWith;
158
+ const v1 = fields[field], v2 = fields[partner];
159
+ if (v1 || v2) {
160
+ allValues.push({ text: mergeRules[field].fn(v1, v2), section, field });
161
+ }
162
+ continue;
163
+ }
164
+ if (fields[field]) {
165
+ const val = fields[field];
166
+ if (field === 'dialogue') {
167
+ let lines = val;
168
+ if (!lines.startsWith('"') && !lines.startsWith('\u201c')) lines = `"${lines}"`;
169
+ allValues.push({ text: `Dialogue: ${lines}`, section, field });
170
+ } else if (Array.isArray(val)) {
171
+ allValues.push({ text: nlJoin(val), section, field });
172
+ } else {
173
+ allValues.push({ text: val, section, field });
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ if (allValues.length === 0) return '';
180
+
181
+ // Assemble with merge groups
182
+ const gearFields = new Set(['camera_body', 'focal_length', 'lens_filter']);
183
+ const segments = [];
184
+ let subjectBuf = [], gearBuf = [];
185
+ const flushSubject = () => { if (subjectBuf.length) { segments.push(subjectBuf.map((s, i) => i === 0 ? s.text : (s.field === 'framing' ? '; ' + s.text : ', ' + s.text)).join('')); subjectBuf = []; } };
186
+ const flushGear = () => { if (gearBuf.length) { segments.push(gearBuf.map(g => g.text).join(', ')); gearBuf = []; } };
187
+
188
+ for (const v of allValues) {
189
+ if (v.section === 'SUBJECT') { flushGear(); subjectBuf.push(v); }
190
+ else if (v.section === 'CINEMATOGRAPHY' && gearFields.has(v.field)) { flushSubject(); gearBuf.push(v); }
191
+ else { flushSubject(); flushGear(); segments.push(v.text); }
192
+ }
193
+ flushSubject(); flushGear();
194
+
195
+ return segments.map(s => {
196
+ let t = s.charAt(0).toUpperCase() + s.slice(1);
197
+ if (!t.endsWith('.') && !t.endsWith('!') && !t.endsWith('"')) t += '.';
198
+ return t;
199
+ }).join(' ');
200
+ }
package/lib/share.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CinePrompt share link creation via Supabase RPC.
3
+ * Requires a CinePrompt API key (Pro subscribers).
4
+ */
5
+
6
+ const SUPABASE_URL = 'https://jbeuvbsremektkwqmnps.supabase.co';
7
+ const ANON_KEY = 'sb_publishable_W-tmZXUJsPIwjMBQVeH2bw_VIIS5PWw';
8
+
9
+ export async function createShareLink(apiKey, stateJson, promptText, mode) {
10
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/create_share_link`, {
11
+ method: 'POST',
12
+ headers: {
13
+ 'apikey': ANON_KEY,
14
+ 'Authorization': `Bearer ${ANON_KEY}`,
15
+ 'Content-Type': 'application/json'
16
+ },
17
+ body: JSON.stringify({
18
+ api_key: apiKey,
19
+ prompt_text: promptText,
20
+ state_json: stateJson,
21
+ share_mode: mode || 'single'
22
+ })
23
+ });
24
+
25
+ if (!res.ok) {
26
+ const err = await res.text();
27
+ throw new Error(`CinePrompt API error: ${err}`);
28
+ }
29
+
30
+ const data = await res.json();
31
+ return {
32
+ url: data.url,
33
+ shortCode: data.short_code
34
+ };
35
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "cineprompt",
3
+ "version": "1.0.0",
4
+ "description": "AI video prompt builder for cinematographers. Create structured prompts and share links via cineprompt.io.",
5
+ "keywords": ["ai", "video", "prompt", "cinematography", "sora", "runway", "kling", "veo", "cineprompt"],
6
+ "author": "Light Owl, LLC",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/lightowl/cineprompt-cli"
11
+ },
12
+ "homepage": "https://cineprompt.io",
13
+ "bin": {
14
+ "cineprompt": "./bin/cineprompt.js"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "lib/",
19
+ "data/field-values.json",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "type": "module"
27
+ }