@nodaro/prompts 1.29.0 → 1.32.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.
- package/dist/index.cjs +232 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -2
- package/dist/index.d.ts +84 -2
- package/dist/index.js +223 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/caption-treatments.test.ts +155 -0
- package/src/__tests__/creator-presets.test.ts +151 -0
- package/src/__tests__/factory-presets.test.ts +63 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +32 -0
- package/src/__tests__/node-send-text.test.ts +91 -0
- package/src/__tests__/transitions-blended-cut.test.ts +3 -1
- package/src/audio-tags.ts +36 -0
- package/src/caption-treatments.ts +109 -0
- package/src/factory-presets/sfx.ts +21 -0
- package/src/factory-presets/text.ts +29 -0
- package/src/factory-presets/video-edit.ts +14 -0
- package/src/factory-presets/voice.ts +14 -0
- package/src/index.ts +5 -0
- package/src/node-send-text.ts +120 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// packages/prompts/src/caption-treatments.ts
|
|
2
|
+
import type { CaptionPlan, SupportedFontName } from "@nodaro/shared"
|
|
3
|
+
import { ADD_CAPTIONS_PRESETS } from "./factory-presets/video-edit.js"
|
|
4
|
+
|
|
5
|
+
export const HOOK_PLATE_PRESET_ID = "add-captions/hook-plate"
|
|
6
|
+
export const BODY_CAPTIONS_PRESET_ID = "add-captions/body-captions"
|
|
7
|
+
|
|
8
|
+
/** Levers a spoken language needs on every caption segment; a language with no entry adds none. */
|
|
9
|
+
export interface CaptionLanguageLevers {
|
|
10
|
+
readonly fontFamily?: SupportedFontName
|
|
11
|
+
readonly uppercase?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Keyed by primary language tag. Hebrew: one face that carries Hebrew and Latin, so a Latin
|
|
16
|
+
* brand inside a Hebrew line is drawn in the same face, and no upper-casing, so the brand keeps
|
|
17
|
+
* its spelling.
|
|
18
|
+
*/
|
|
19
|
+
export const CAPTION_LEVERS_BY_LANGUAGE: Readonly<Record<string, CaptionLanguageLevers>> = {
|
|
20
|
+
he: { fontFamily: "Rubik", uppercase: false },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The add-captions segment keys a lever set may carry: the segment schema minus times and words. */
|
|
24
|
+
export const CAPTION_SEGMENT_LEVER_KEYS: readonly string[] = [
|
|
25
|
+
"style", "position", "fontSize", "color", "backgroundColor", "look", "fontFamily", "fontWeight",
|
|
26
|
+
"strokeColor", "strokeWidth", "highlightColor", "uppercase", "positionY", "animate", "maxWordsPerLine",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export type CaptionSegmentRole = "hook" | "body"
|
|
30
|
+
|
|
31
|
+
export interface HookPlateSegmentsOptions {
|
|
32
|
+
/** The body's levers; default = the Body Captions preset. Keys outside CAPTION_SEGMENT_LEVER_KEYS are ignored. */
|
|
33
|
+
readonly bodyLevers?: Readonly<Record<string, unknown>>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HookPlateSegmentsResult {
|
|
37
|
+
/** Route-shaped (camelCase) `segments`. Empty means "no captions". */
|
|
38
|
+
readonly segments: Record<string, unknown>[]
|
|
39
|
+
/** Segments left out, in order hook, body. */
|
|
40
|
+
readonly dropped: CaptionSegmentRole[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const LEVER_KEYS: ReadonlySet<string> = new Set(CAPTION_SEGMENT_LEVER_KEYS)
|
|
44
|
+
|
|
45
|
+
/** A lever set: segment lever keys only, without null or undefined values. */
|
|
46
|
+
function pickLevers(source: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> {
|
|
47
|
+
const out: Record<string, unknown> = {}
|
|
48
|
+
if (!source) return out
|
|
49
|
+
for (const [k, v] of Object.entries(source)) if (LEVER_KEYS.has(k) && v !== undefined && v !== null) out[k] = v
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function presetLevers(id: string): Record<string, unknown> {
|
|
54
|
+
const preset = ADD_CAPTIONS_PRESETS.find((p) => p.id === id)
|
|
55
|
+
if (!preset) throw new Error(`The ${id} caption preset is missing from the catalog.`)
|
|
56
|
+
return pickLevers(preset.data)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const PLATE_LEVERS = presetLevers(HOOK_PLATE_PRESET_ID)
|
|
60
|
+
const BODY_LEVERS = presetLevers(BODY_CAPTIONS_PRESET_ID)
|
|
61
|
+
|
|
62
|
+
function languageLevers(tag: string | undefined): CaptionLanguageLevers {
|
|
63
|
+
if (typeof tag !== "string") return {}
|
|
64
|
+
const primary = tag.trim().split(/[-_]/)[0]!.toLowerCase()
|
|
65
|
+
return Object.hasOwn(CAPTION_LEVERS_BY_LANGUAGE, primary) ? CAPTION_LEVERS_BY_LANGUAGE[primary]! : {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isTime = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* One caption plan → the `segments` of one add-captions request: the opening line as the Hook
|
|
72
|
+
* Plate, then the body's words in the Body Captions style (or the caller's own body levers).
|
|
73
|
+
* The styling is read from the two presets, never restated here. Pure: no I/O.
|
|
74
|
+
*/
|
|
75
|
+
export function hookPlateCaptionSegments(plan: CaptionPlan, opts: HookPlateSegmentsOptions = {}): HookPlateSegmentsResult {
|
|
76
|
+
const hookEnd = plan.hookEndMs
|
|
77
|
+
if (!isTime(hookEnd) || hookEnd < 0) return { segments: [], dropped: ["hook", "body"] }
|
|
78
|
+
const language = languageLevers(plan.language)
|
|
79
|
+
const limit = typeof plan.videoDurationMs === "number" ? plan.videoDurationMs : Number.POSITIVE_INFINITY
|
|
80
|
+
const segments: Record<string, unknown>[] = []
|
|
81
|
+
const dropped: CaptionSegmentRole[] = []
|
|
82
|
+
|
|
83
|
+
const plateEnd = Math.min(hookEnd, limit)
|
|
84
|
+
if (plateEnd > 0 && typeof plan.hookText === "string" && /\S/.test(plan.hookText)) {
|
|
85
|
+
segments.push({ startMs: 0, endMs: plateEnd, text: plan.hookText, ...PLATE_LEVERS, ...language })
|
|
86
|
+
} else {
|
|
87
|
+
dropped.push("hook")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The body starts where the opening line ends, even when no plate is shown, so it never
|
|
91
|
+
// draws over the opening line.
|
|
92
|
+
const bodyEnd = Math.min(plan.bodyEndMs, limit)
|
|
93
|
+
const words = Array.isArray(plan.captions) ? plan.captions : []
|
|
94
|
+
const chosen = pickLevers(opts.bodyLevers)
|
|
95
|
+
const bodyLevers = Object.keys(chosen).length > 0 ? chosen : BODY_LEVERS
|
|
96
|
+
if (bodyEnd > hookEnd && words.length > 0) {
|
|
97
|
+
segments.push({
|
|
98
|
+
startMs: hookEnd,
|
|
99
|
+
endMs: bodyEnd,
|
|
100
|
+
captions: words.map((w) => ({ text: w.text, startMs: w.startMs, endMs: w.endMs })),
|
|
101
|
+
...bodyLevers,
|
|
102
|
+
...language,
|
|
103
|
+
})
|
|
104
|
+
} else {
|
|
105
|
+
dropped.push("body")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { segments, dropped }
|
|
109
|
+
}
|
|
@@ -53,6 +53,27 @@ export const TEXT_TO_AUDIO_PRESETS: readonly FactoryPreset[] = [
|
|
|
53
53
|
group: "Ambiences (loopable)",
|
|
54
54
|
data: { provider: "elevenlabs-sfx", duration: 22, loop: true, promptInfluence: 0.5, prompt: "low sci-fi ambient drone, ominous spaceship hum" },
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
id: "text-to-audio/living-room-tone",
|
|
58
|
+
name: "Living Room Tone",
|
|
59
|
+
description: "Quiet, lived-in room.",
|
|
60
|
+
group: "Ambiences (loopable)",
|
|
61
|
+
data: { provider: "elevenlabs-sfx", duration: 22, loop: true, promptInfluence: 0.4, prompt: "quiet living room room tone, faint air and a soft distant household hum, steady and even, an empty room at rest" },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "text-to-audio/kitchen-hum",
|
|
65
|
+
name: "Kitchen Hum",
|
|
66
|
+
description: "Fridge hum in a still kitchen.",
|
|
67
|
+
group: "Ambiences (loopable)",
|
|
68
|
+
data: { provider: "elevenlabs-sfx", duration: 22, loop: true, promptInfluence: 0.4, prompt: "home kitchen room tone, a low steady refrigerator hum with faint air, calm and continuous, an empty kitchen at rest" },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "text-to-audio/cafe-murmur",
|
|
72
|
+
name: "Café Murmur",
|
|
73
|
+
description: "Soft, indistinct café chatter.",
|
|
74
|
+
group: "Ambiences (loopable)",
|
|
75
|
+
data: { provider: "elevenlabs-sfx", duration: 22, loop: true, promptInfluence: 0.4, prompt: "café background ambience, a soft indistinct murmur of distant conversation and the occasional faint cup clink, steady and even, no clear words" },
|
|
76
|
+
},
|
|
56
77
|
|
|
57
78
|
// ── UI & Stingers ────────────────────────────────────────────────────────
|
|
58
79
|
{
|
|
@@ -80,6 +80,24 @@ export const LLM_CHAT_PRESETS: readonly FactoryPreset[] = [
|
|
|
80
80
|
temperature: 0.7,
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
id: "llm-chat/hook-generator",
|
|
85
|
+
name: "Hook Generator",
|
|
86
|
+
description: "10 opening lines for a talking-to-camera video.",
|
|
87
|
+
group: "Writing & Marketing",
|
|
88
|
+
data: {
|
|
89
|
+
systemPrompt:
|
|
90
|
+
"You write opening lines for short vertical videos in which one person talks straight to the camera about a product, a site or an idea. Given the user's topic, write 10 different opening lines.\n\n" +
|
|
91
|
+
"Every line:\n" +
|
|
92
|
+
"- is one short spoken clause of at most 8 words;\n" +
|
|
93
|
+
"- leads with the payload the viewer can check: a number, a price, a time saved, a before and after, a contrast with the usual way, or a named result;\n" +
|
|
94
|
+
"- opens on the claim itself — no greeting, no filler word, no introduction of the product before the claim;\n" +
|
|
95
|
+
"- sounds said, not written: plain words, contractions welcome, no hashtags, no emoji, no stage directions;\n" +
|
|
96
|
+
"- spells the product or brand exactly as the user gives it and writes numbers as digits.\n\n" +
|
|
97
|
+
"Vary the angle across the 10: a surprising number, a price, time saved, a mistake to avoid, a before and after, a contrast, a named result, a direct question. Write in the language the user writes in. Return only the numbered list, one line each.",
|
|
98
|
+
temperature: 0.9,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
83
101
|
|
|
84
102
|
// ── Utility ──────────────────────────────────────────────────────────────
|
|
85
103
|
{
|
|
@@ -273,6 +291,17 @@ export const IMAGE_TO_TEXT_PRESETS: readonly FactoryPreset[] = [
|
|
|
273
291
|
group: "Extraction",
|
|
274
292
|
data: { detailLevel: "detailed", customPrompt: "Write a compelling e-commerce product description based on this product image." },
|
|
275
293
|
},
|
|
294
|
+
{
|
|
295
|
+
id: "image-to-text/site-screenshot-summary",
|
|
296
|
+
name: "Site Screenshot Summary",
|
|
297
|
+
description: "Section, headings and figures from a web page.",
|
|
298
|
+
group: "Extraction",
|
|
299
|
+
data: {
|
|
300
|
+
detailLevel: "structured",
|
|
301
|
+
customPrompt:
|
|
302
|
+
"Describe this website screenshot as five labelled lines. Section: which part of the site it is (for example the top of the home page, a feature, how it works, reviews, pricing, sign-up, navigation or footer). Headings: every heading exactly as written. Figures: every number, price, rating and date exactly as written, each with what it refers to. Brand: the product or company name exactly as spelled. First look: what a viewer notices first, in one sentence. Write \"none\" on a line with nothing to report.",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
276
305
|
|
|
277
306
|
// ── Creative ─────────────────────────────────────────────────────────────
|
|
278
307
|
{
|
|
@@ -95,6 +95,20 @@ export const ADD_CAPTIONS_PRESETS: readonly FactoryPreset[] = [
|
|
|
95
95
|
group: "Caption Styles",
|
|
96
96
|
data: { style: "subtitle", position: "top", fontSize: 36, color: "#FFFFFF", autoTranscribe: true },
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
id: "add-captions/hook-plate",
|
|
100
|
+
name: "Hook Plate",
|
|
101
|
+
description: "Your opening line as a bold plate near the top.",
|
|
102
|
+
group: "Caption Styles",
|
|
103
|
+
data: { style: "subtitle", look: "clean", position: "top", positionY: 20, fontSize: 32, fontWeight: 800, maxWordsPerLine: 4, color: "#111111", backgroundColor: "#FFFFFF", autoTranscribe: false },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "add-captions/body-captions",
|
|
107
|
+
name: "Body Captions",
|
|
108
|
+
description: "Word-by-word captions under the face.",
|
|
109
|
+
group: "Caption Styles",
|
|
110
|
+
data: { style: "word-highlight", look: "outline", position: "bottom", positionY: 72, fontSize: 32, maxWordsPerLine: 3, color: "#FFFFFF", autoTranscribe: true },
|
|
111
|
+
},
|
|
98
112
|
]
|
|
99
113
|
|
|
100
114
|
export const COMBINE_VIDEOS_PRESETS: readonly FactoryPreset[] = [
|
|
@@ -115,6 +115,20 @@ export const VOICE_DESIGN_PRESETS: readonly FactoryPreset[] = [
|
|
|
115
115
|
group: "Narration & Character",
|
|
116
116
|
data: { voiceDescription: "A calm, soft-spoken meditation guide with a slow, breathy, reassuring delivery." },
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
id: "voice-design/casual-creator-female",
|
|
120
|
+
name: "Casual Creator (female)",
|
|
121
|
+
description: "Relaxed, warm, everyday.",
|
|
122
|
+
group: "Narration & Character",
|
|
123
|
+
data: { voiceDescription: "An adult woman in her late twenties talking casually to the viewer as if telling a friend about something she tried: relaxed, warm and upbeat, a natural conversational pace with small pauses, a light neutral American accent, recorded close and dry in a quiet room, never polished or announcer-like." },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "voice-design/casual-creator-male",
|
|
127
|
+
name: "Casual Creator (male)",
|
|
128
|
+
description: "Relaxed, warm, everyday.",
|
|
129
|
+
group: "Narration & Character",
|
|
130
|
+
data: { voiceDescription: "An adult man in his late twenties talking casually to the viewer as if telling a friend about something he tried: relaxed, warm and upbeat, a natural conversational pace with small pauses, a light neutral American accent, recorded close and dry in a quiet room, never polished or announcer-like." },
|
|
131
|
+
},
|
|
118
132
|
|
|
119
133
|
// ── Professional & Assistant ─────────────────────────────────────────────
|
|
120
134
|
{
|
package/src/index.ts
CHANGED
|
@@ -86,8 +86,10 @@ export * from "./image-reference-doctrine.js"
|
|
|
86
86
|
export * from "./scene3d-reference-doctrine.js"
|
|
87
87
|
export * from "./prompt-wizard-categories.js"
|
|
88
88
|
export * from "./resolve-prompt.js"
|
|
89
|
+
export { stripAudioTags } from "./audio-tags.js"
|
|
89
90
|
export * from "./node-prompt-fields.js"
|
|
90
91
|
export * from "./factory-presets.js"
|
|
92
|
+
export * from "./caption-treatments.js"
|
|
91
93
|
export * from "./style-presets.js"
|
|
92
94
|
export * from "./object-asset-presets.js"
|
|
93
95
|
export * from "./factory-snippets/index.js"
|
|
@@ -103,3 +105,6 @@ export * from "./ad-creative-analysis.js"
|
|
|
103
105
|
|
|
104
106
|
// --- Picker art: option pictures + topic icons (editor and API) ---
|
|
105
107
|
export * from "./picker-art/index.js"
|
|
108
|
+
|
|
109
|
+
// --- What a text-requiring node would send (the orchestrator's empty-input skip reads it) ---
|
|
110
|
+
export * from "./node-send-text.js"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SOCIAL_POST_NODE_TYPES } from "@nodaro/shared"
|
|
2
|
+
import { computeLlmChatFields, computeNodePrompt, computeScriptTopic } from "./resolve-prompt.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Output nodes that send exactly what is WIRED into them — a webhook's
|
|
6
|
+
* parameters, a social post's caption and media. When every wire produced
|
|
7
|
+
* nothing in this run (a Filter List that kept no row, a node that was itself
|
|
8
|
+
* skipped), there is nothing to send: the node is skipped with
|
|
9
|
+
* `skipReason: "empty_input"` instead of posting an empty payload (a site that
|
|
10
|
+
* validates its input answers 400 and alerts its owner) or failing the run
|
|
11
|
+
* (a publish route refuses a post with no text and no media). A typed caption
|
|
12
|
+
* with no wire at all is the author's choice and still runs, as before. ONE
|
|
13
|
+
* list, read by the orchestrator's skip rule (`empty-input-skips.ts`).
|
|
14
|
+
*/
|
|
15
|
+
export const WIRED_OUTPUT_NODE_TYPES: ReadonlySet<string> = new Set(["webhook-output", ...SOCIAL_POST_NODE_TYPES])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Node types that send TEXT to a model or a voice and fail on an empty one —
|
|
19
|
+
* `userInput: Too small` (llm-chat), "no text found" (text-to-speech) — so a
|
|
20
|
+
* run with nothing upstream can skip them instead of failing. Never an image
|
|
21
|
+
* or video node (an empty prompt is a legal request there) and never a social
|
|
22
|
+
* post (an empty caption is the author's choice). ONE list, read by the
|
|
23
|
+
* orchestrator's skip rule (`empty-input-skips.ts`); the engine never names a
|
|
24
|
+
* type on its own.
|
|
25
|
+
*/
|
|
26
|
+
export const TEXT_REQUIRED_NODE_TYPES: ReadonlySet<string> = new Set([
|
|
27
|
+
"llm-chat",
|
|
28
|
+
"ai-writer",
|
|
29
|
+
"generate-script",
|
|
30
|
+
"text-to-speech",
|
|
31
|
+
"generate-music",
|
|
32
|
+
"text-to-audio",
|
|
33
|
+
// Save to Collection refuses an empty record ("nothing to save"): a feed
|
|
34
|
+
// with nothing new wired straight into it must skip it, not fail the run.
|
|
35
|
+
"collection-write",
|
|
36
|
+
// Generate Image: an empty prompt is a legal request in general (references
|
|
37
|
+
// alone), but when the text it was WIRED produced nothing this run — the skip
|
|
38
|
+
// rule's precondition — an empty prompt is a paid picture of nothing.
|
|
39
|
+
"generate-image",
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The node's data without its prompt pre/post text: the skip decision is about
|
|
44
|
+
* the CORE text. A prefix alone ("Write a news item about:") is not something
|
|
45
|
+
* to send — with the affixes applied, an empty core read as non-empty and an
|
|
46
|
+
* idle tick paid for a model call on the prefix.
|
|
47
|
+
*/
|
|
48
|
+
const withoutAffixes = (data: Record<string, unknown>): Record<string, unknown> => ({ ...data, promptPrefix: undefined, promptSuffix: undefined })
|
|
49
|
+
|
|
50
|
+
export interface NodeSendTextArgs {
|
|
51
|
+
/** A list fan-out item (highest precedence). */
|
|
52
|
+
override?: string
|
|
53
|
+
/** The text wired into the node's main text input. */
|
|
54
|
+
wired?: string
|
|
55
|
+
/** llm-chat only: the text wired into `system-prompt`. */
|
|
56
|
+
wiredSystemPrompt?: string
|
|
57
|
+
refMap: ReadonlyMap<string, string>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const present = (s?: string): s is string => typeof s === "string" && s.trim().length > 0
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* ai-writer's user input — the rule its sync-HTTP body applies: a fan-out
|
|
64
|
+
* item, else the wired text, else the typed `userInput`, else the legacy
|
|
65
|
+
* `prompt`. (ai-writer is renamed to llm-chat on load; saved graphs that
|
|
66
|
+
* never reloaded still run it.)
|
|
67
|
+
*/
|
|
68
|
+
export function computeAiWriterInput(
|
|
69
|
+
data: Record<string, unknown>,
|
|
70
|
+
{ override, wired }: Pick<NodeSendTextArgs, "override" | "wired">,
|
|
71
|
+
): string {
|
|
72
|
+
return [override, wired, data.userInput as string | undefined, data.prompt as string | undefined].find(present) ?? ""
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The text a text-requiring node would send, by the SAME rule its executor
|
|
77
|
+
* uses (`computeLlmChatFields`, `computeScriptTopic`, `computeNodePrompt`,
|
|
78
|
+
* `computeAiWriterInput`) — so "would this node run on nothing?" is answered
|
|
79
|
+
* by the function that decides what it sends, not by a second reading of it.
|
|
80
|
+
* Undefined for any other node type.
|
|
81
|
+
*/
|
|
82
|
+
export function computeNodeSendText(
|
|
83
|
+
nodeType: string,
|
|
84
|
+
data: Record<string, unknown>,
|
|
85
|
+
args: NodeSendTextArgs,
|
|
86
|
+
): string | undefined {
|
|
87
|
+
const bare = withoutAffixes(data)
|
|
88
|
+
switch (nodeType) {
|
|
89
|
+
case "llm-chat":
|
|
90
|
+
return computeLlmChatFields(bare, {
|
|
91
|
+
override: args.override,
|
|
92
|
+
wiredUserInput: args.wired,
|
|
93
|
+
wiredSystemPrompt: args.wiredSystemPrompt,
|
|
94
|
+
refMap: args.refMap,
|
|
95
|
+
}).userInput
|
|
96
|
+
case "ai-writer":
|
|
97
|
+
return computeAiWriterInput(bare, args)
|
|
98
|
+
case "generate-script":
|
|
99
|
+
return computeScriptTopic(bare, { override: args.override, wired: args.wired, refMap: args.refMap })
|
|
100
|
+
case "generate-music": {
|
|
101
|
+
// A music node sings typed lyrics with no prompt; only both empty is nothing to send.
|
|
102
|
+
const prompt = computeNodePrompt(nodeType, bare, { override: args.override, wired: args.wired, refMap: args.refMap })
|
|
103
|
+
return present(prompt) ? prompt : present(data.lyrics as string | undefined) ? (data.lyrics as string) : ""
|
|
104
|
+
}
|
|
105
|
+
case "text-to-speech":
|
|
106
|
+
case "text-to-audio":
|
|
107
|
+
case "generate-image":
|
|
108
|
+
return computeNodePrompt(nodeType, bare, { override: args.override, wired: args.wired, refMap: args.refMap })
|
|
109
|
+
case "collection-write": {
|
|
110
|
+
// The record's content: the item (a fan-out row, else what reached `in`),
|
|
111
|
+
// else a title, text or link typed on the node. A picture alone is a
|
|
112
|
+
// record too, but it never starves the node: only a wired TEXT input that
|
|
113
|
+
// produced nothing can (the skip rule's precondition).
|
|
114
|
+
const typed = [data.title, data.text, data.link].find((v): v is string => present(v as string | undefined))
|
|
115
|
+
return [args.override, args.wired, typed].find(present) ?? ""
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
}
|