jbilcke-hf HF staff commited on
Commit
c32ec0d
1 Parent(s): 797cbb8
public/layouts/layout0.jpg CHANGED
public/layouts/layout0_hd.jpg ADDED
public/layouts/layout1.jpg CHANGED
public/layouts/layout1_hd.jpg ADDED
public/layouts/layout2.jpg CHANGED
public/layouts/layout2_hd.jpg ADDED
public/layouts/layout3 hd.jpg ADDED
public/layouts/layout3.jpg CHANGED
src/app/globals.css CHANGED
@@ -30,4 +30,10 @@ body {
30
  /* this is the trick to bypass the style={{}} attribute when printing */
31
  @media print {
32
  .comic-page[style] { width: 100vw !important; }
33
- }
 
 
 
 
 
 
 
30
  /* this is the trick to bypass the style={{}} attribute when printing */
31
  @media print {
32
  .comic-page[style] { width: 100vw !important; }
33
+ }
34
+
35
+
36
+ .render-to-image .comic-panel {
37
+ height: auto !important;
38
+ /* max-width: fit-content !important; */
39
+ }
src/app/interface/panel/index.tsx CHANGED
@@ -35,7 +35,11 @@ export function Panel({
35
  const panels = useStore(state => state.panels)
36
  const prompt = panels[panel] || ""
37
 
 
 
 
38
  const zoomLevel = useStore(state => state.zoomLevel)
 
39
 
40
  // const setCaption = useStore(state => state.setCaption)
41
  // const captions = useStore(state => state.captions)
@@ -179,6 +183,7 @@ export function Panel({
179
  */
180
 
181
  const frameClassName = cn(
 
182
  `w-full h-full`,
183
  `border-stone-800`,
184
  `transition-all duration-200 ease-in-out`,
@@ -214,7 +219,6 @@ export function Panel({
214
  }, [rendered.assetUrl, ref.current])
215
  */
216
 
217
-
218
  if (prompt && !rendered.assetUrl) {
219
  return (
220
  <div className={cn(
@@ -227,6 +231,7 @@ export function Panel({
227
  )
228
  }
229
 
 
230
  return (
231
  <div className={cn(
232
  frameClassName,
@@ -240,8 +245,33 @@ export function Panel({
240
  width={width}
241
  height={height}
242
  alt={rendered.alt}
243
- className="w-full object-cover md:h-full md:max-w-fit print:w-full print:object-cover"
 
 
 
244
  />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </div>
246
  )
247
  }
 
35
  const panels = useStore(state => state.panels)
36
  const prompt = panels[panel] || ""
37
 
38
+ const captions = useStore(state => state.captions)
39
+ const caption = captions[panel] || ""
40
+
41
  const zoomLevel = useStore(state => state.zoomLevel)
42
+ const showCaptions = useStore(state => state.showCaptions)
43
 
44
  // const setCaption = useStore(state => state.setCaption)
45
  // const captions = useStore(state => state.captions)
 
183
  */
184
 
185
  const frameClassName = cn(
186
+ //`flex`,
187
  `w-full h-full`,
188
  `border-stone-800`,
189
  `transition-all duration-200 ease-in-out`,
 
219
  }, [rendered.assetUrl, ref.current])
220
  */
221
 
 
222
  if (prompt && !rendered.assetUrl) {
223
  return (
224
  <div className={cn(
 
231
  )
232
  }
233
 
234
+
235
  return (
236
  <div className={cn(
237
  frameClassName,
 
245
  width={width}
246
  height={height}
247
  alt={rendered.alt}
248
+ className={cn(
249
+ `comic-panel w-full h-full object-cover max-w-max`,
250
+ // showCaptions ? `-mt-11` : ''
251
+ )}
252
  />}
253
+ {/*
254
+ <div className={cn(
255
+ `flex`,
256
+ `bg-stone-50`,
257
+ `border-stone-800`,
258
+ `transition-all duration-200 ease-in-out`,
259
+ zoomLevel > 140 ? `border-b-[2px] md:border-b-[4px]` :
260
+ zoomLevel > 120 ? `border-b-[1.5px] md:border-b-[3px]` :
261
+ zoomLevel > 90 ? `border-b-[1px] md:border-b-[2px]` :
262
+ zoomLevel > 40 ? `border-b-[0.5px] md:border-b-[1px]` :
263
+ `border-transparent md:border-b-[0.5px]`,
264
+ `print:border-b-[1.5px]`,
265
+ showCaptions ? `` : `hidden`,
266
+ `truncate`,
267
+ `h-11`,
268
+ `p-3`
269
+ )}
270
+ style={{
271
+ fontSize: zoomLevel * 0.2
272
+ }}
273
+ >{caption}</div>
274
+ */}
275
  </div>
276
  )
277
  }
src/app/interface/top-menu/index.tsx CHANGED
@@ -25,6 +25,7 @@ import layoutPreview1 from "../../../../public/layouts/layout1.jpg"
25
  import layoutPreview2 from "../../../../public/layouts/layout2.jpg"
26
  import layoutPreview3 from "../../../../public/layouts/layout3.jpg"
27
  import { StaticImageData } from "next/image"
 
28
 
29
  const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
30
  Layout0: layoutPreview0,
@@ -41,6 +42,9 @@ export function TopMenu() {
41
  const layout = useStore(state => state.layout)
42
  const setLayout = useStore(state => state.setLayout)
43
 
 
 
 
44
  const generate = useStore(state => state.generate)
45
 
46
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
@@ -200,6 +204,15 @@ export function TopMenu() {
200
  >
201
  Generate
202
  </Button>
 
 
 
 
 
 
 
 
 
203
  </div>
204
  {/*
205
  Let's add this feature later, because right now people
 
25
  import layoutPreview2 from "../../../../public/layouts/layout2.jpg"
26
  import layoutPreview3 from "../../../../public/layouts/layout3.jpg"
27
  import { StaticImageData } from "next/image"
28
+ import { Switch } from "@/components/ui/switch"
29
 
30
  const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
31
  Layout0: layoutPreview0,
 
42
  const layout = useStore(state => state.layout)
43
  const setLayout = useStore(state => state.setLayout)
44
 
45
+ const setShowCaptions = useStore(state => state.setShowCaptions)
46
+ const showCaptions = useStore(state => state.showCaptions)
47
+
48
  const generate = useStore(state => state.generate)
49
 
50
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
 
204
  >
205
  Generate
206
  </Button>
207
+ {/*
208
+ <Switch
209
+ checked={showCaptions}
210
+ onCheckedChange={setShowCaptions}
211
+ />
212
+ <Label>
213
+ Caption
214
+ </Label>
215
+ */}
216
  </div>
217
  {/*
218
  Let's add this feature later, because right now people
src/app/interface/zoom/index.tsx CHANGED
@@ -15,7 +15,7 @@ export function Zoom() {
15
  `animation-all duration-300 ease-in-out`,
16
  isGeneratingStory ? `scale-0 opacity-0` : ``,
17
  )}>
18
- <div className="font-mono text-xs pb-1 text-stone-700 bg-stone-50 rounded-full">
19
  Zoom
20
  </div>
21
  <div className="w-2">
 
15
  `animation-all duration-300 ease-in-out`,
16
  isGeneratingStory ? `scale-0 opacity-0` : ``,
17
  )}>
18
+ <div className="font-mono text-xs pb-1 text-stone-700 bg-stone-50 p-1 rounded-sm">
19
  Zoom
20
  </div>
21
  <div className="w-2">
src/app/main.tsx CHANGED
@@ -25,6 +25,7 @@ export default function Main() {
25
  const setLayouts = useStore(state => state.setLayouts)
26
 
27
  const setPanels = useStore(state => state.setPanels)
 
28
 
29
  const zoomLevel = useStore(state => state.zoomLevel)
30
 
@@ -49,13 +50,16 @@ export default function Main() {
49
 
50
  const nbPanels = 4
51
  const newPanels: string[] = []
 
52
  setWaitABitMore(true)
53
 
54
  for (let p = 0; p < nbPanels; p++) {
55
- const newPanel = [panelPromptPrefix, llmResponse[p] || ""]
 
56
  newPanels.push(newPanel.map(chunk => chunk).join(", "))
57
  }
58
  console.log("newPanels:", newPanels)
 
59
  setPanels(newPanels)
60
  } catch (err) {
61
  console.error(err)
 
25
  const setLayouts = useStore(state => state.setLayouts)
26
 
27
  const setPanels = useStore(state => state.setPanels)
28
+ const setCaptions = useStore(state => state.setCaptions)
29
 
30
  const zoomLevel = useStore(state => state.zoomLevel)
31
 
 
50
 
51
  const nbPanels = 4
52
  const newPanels: string[] = []
53
+ const newCaptions: string[] = []
54
  setWaitABitMore(true)
55
 
56
  for (let p = 0; p < nbPanels; p++) {
57
+ newCaptions.push(llmResponse[p]?.caption || "...")
58
+ const newPanel = [panelPromptPrefix, llmResponse[p]?.instructions || ""]
59
  newPanels.push(newPanel.map(chunk => chunk).join(", "))
60
  }
61
  console.log("newPanels:", newPanels)
62
+ setCaptions(newCaptions)
63
  setPanels(newPanels)
64
  } catch (err) {
65
  console.error(err)
src/app/queries/getStory.ts CHANGED
@@ -5,6 +5,8 @@ import { dirtyCaptionCleaner } from "@/lib/dirtyCaptionCleaner"
5
 
6
  import { predict } from "./predict"
7
  import { Preset } from "../engine/presets"
 
 
8
 
9
  export const getStory = async ({
10
  preset,
@@ -12,17 +14,17 @@ export const getStory = async ({
12
  }: {
13
  preset: Preset;
14
  prompt: string;
15
- }): Promise<string[]> => {
16
 
17
  const query = createLlamaPrompt([
18
  {
19
  role: "system",
20
  content: [
21
  `You are a comic book author specialized in ${preset.llmPrompt}`,
22
- `Please write detailed drawing instructions for the 5 panels of a new silent comic book page.`,
23
- `Give your response as a JSON array like this: \`Array<{ panel: number; caption: string}>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
- `Be brief in your 5 captions, don't add your own comments. Be straight to the point, and never reply things like "Sure, I can.." etc.`
26
  ].filter(item => item).join("\n")
27
  },
28
  {
@@ -53,44 +55,28 @@ export const getStory = async ({
53
  }
54
 
55
  console.log("Raw response from LLM:", result)
56
- const tmp = dirtyLLMResponseCleaner(result)
57
 
58
- let captions: string[] = []
59
 
60
  try {
61
- captions = dirtyLLMJsonParser(tmp)
62
  } catch (err) {
63
  console.log(`failed to read LLM response: ${err}`)
64
 
65
- // it is possible that the LLM has generated multiple JSON files like this:
66
-
67
- /*
68
- [ {
69
- "panel": 1,
70
- "caption": "A samurai stands at the edge of a bustling street in San Francisco, looking out of place among the hippies and beatniks."
71
- } ]
72
-
73
- [ {
74
- "panel": 2,
75
- "caption": "The samurai spots a group of young people playing music on the sidewalk. He approaches them, intrigued."
76
- } ]
77
- */
78
- try {
79
- // in that case, we can try to repair it like so:
80
- let strategy2 = `[${tmp.split("[").pop() || ""}`
81
- strategy2.replaceAll("[", ",")
82
-
83
- captions = dirtyLLMJsonParser(strategy2)
84
- } catch (err2) {
85
  // in case of failure here, it might be because the LLM hallucinated a completely different response,
86
  // such as markdown. There is no real solution.. but we can try a fallback:
87
 
88
- captions = (
89
- tmp.split("*")
90
- .map(item => item.replaceAll("[", "[").replaceAll("]", "]").trim())
91
- )
92
- }
 
 
 
 
93
  }
94
 
95
- return captions.map(caption => dirtyCaptionCleaner(caption))
96
  }
 
5
 
6
  import { predict } from "./predict"
7
  import { Preset } from "../engine/presets"
8
+ import { LLMResponse } from "@/types"
9
+ import { cleanJson } from "@/lib/cleanJson"
10
 
11
  export const getStory = async ({
12
  preset,
 
14
  }: {
15
  preset: Preset;
16
  prompt: string;
17
+ }): Promise<LLMResponse> => {
18
 
19
  const query = createLlamaPrompt([
20
  {
21
  role: "system",
22
  content: [
23
  `You are a comic book author specialized in ${preset.llmPrompt}`,
24
+ `Please write detailed drawing instructions and a one-sentence short caption for the 4 panels of a new silent comic book page.`,
25
+ `Give your response as a JSON array like this: \`Array<{ panel: number; instructions: string; caption: string}>\`.`,
26
  // `Give your response as Markdown bullet points.`,
27
+ `Be brief in your 4 instructions and captions, don't add your own comments. Be straight to the point, and never reply things like "Sure, I can.." etc.`
28
  ].filter(item => item).join("\n")
29
  },
30
  {
 
55
  }
56
 
57
  console.log("Raw response from LLM:", result)
58
+ const tmp = cleanJson(result)
59
 
60
+ let llmResponse: LLMResponse = []
61
 
62
  try {
63
+ llmResponse = dirtyLLMJsonParser(tmp)
64
  } catch (err) {
65
  console.log(`failed to read LLM response: ${err}`)
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  // in case of failure here, it might be because the LLM hallucinated a completely different response,
68
  // such as markdown. There is no real solution.. but we can try a fallback:
69
 
70
+ llmResponse = (
71
+ tmp.split("*")
72
+ .map(item => item.trim())
73
+ .map((cap, i) => ({
74
+ panel: i,
75
+ caption: cap,
76
+ instructions: cap,
77
+ }))
78
+ )
79
  }
80
 
81
+ return llmResponse.map(res => dirtyCaptionCleaner(res))
82
  }
src/app/queries/predict.ts CHANGED
@@ -17,7 +17,7 @@ export async function predict(inputs: string) {
17
  do_sample: true,
18
 
19
  // hard limit for max_new_tokens is 1512
20
- max_new_tokens: 300, // 1150,
21
  return_full_text: false,
22
  }
23
  })) {
 
17
  do_sample: true,
18
 
19
  // hard limit for max_new_tokens is 1512
20
+ max_new_tokens: 330, // 1150,
21
  return_full_text: false,
22
  }
23
  })) {
src/app/store/index.ts CHANGED
@@ -13,7 +13,8 @@ export const useStore = create<{
13
  preset: Preset
14
  nbFrames: number
15
  panels: string[]
16
- captions: Record<string, string>
 
17
  layout: LayoutName
18
  layouts: LayoutName[]
19
  zoomLevel: number
@@ -26,9 +27,10 @@ export const useStore = create<{
26
  setFont: (font: FontName) => void
27
  setPreset: (preset: Preset) => void
28
  setPanels: (panels: string[]) => void
 
29
  setLayout: (layout: LayoutName) => void
30
  setLayouts: (layouts: LayoutName[]) => void
31
- setCaption: (panelId: number, caption: string) => void
32
  setZoomLevel: (zoomLevel: number) => void
33
  setPage: (page: HTMLDivElement) => void
34
  setGeneratingStory: (isGeneratingStory: boolean) => void
@@ -40,11 +42,12 @@ export const useStore = create<{
40
  }>((set, get) => ({
41
  prompt: "",
42
  font: "actionman",
43
- preset: getPreset("japanese_manga"),
44
  nbFrames: 1,
45
  panels: [],
46
- captions: {},
47
- layout: "Layout1",
 
48
  layouts: getRandomLayoutNames(),
49
  zoomLevel: 60,
50
  page: undefined as unknown as HTMLDivElement,
@@ -81,12 +84,14 @@ export const useStore = create<{
81
  })
82
  },
83
  setPanels: (panels: string[]) => set({ panels }),
84
- setCaption: (panelId: number, caption: string) => {
85
  set({
86
- captions: {
87
- ...get().captions,
88
- [panelId]: caption
89
- }
 
 
90
  })
91
  },
92
  setLayout: (layoutName: LayoutName) => {
@@ -125,6 +130,7 @@ export const useStore = create<{
125
  const { page } = get()
126
  if (!page) { return "" }
127
 
 
128
  const canvas = await html2canvas(page)
129
  console.log("canvas:", canvas)
130
 
@@ -154,7 +160,7 @@ export const useStore = create<{
154
  set({
155
  prompt,
156
  panels: [],
157
- captions: {},
158
  preset: presetName === "random"
159
  ? getRandomPreset()
160
  : getPreset(presetName),
 
13
  preset: Preset
14
  nbFrames: number
15
  panels: string[]
16
+ captions: string[]
17
+ showCaptions: boolean
18
  layout: LayoutName
19
  layouts: LayoutName[]
20
  zoomLevel: number
 
27
  setFont: (font: FontName) => void
28
  setPreset: (preset: Preset) => void
29
  setPanels: (panels: string[]) => void
30
+ setShowCaptions: (showCaptions: boolean) => void
31
  setLayout: (layout: LayoutName) => void
32
  setLayouts: (layouts: LayoutName[]) => void
33
+ setCaptions: (captions: string[]) => void
34
  setZoomLevel: (zoomLevel: number) => void
35
  setPage: (page: HTMLDivElement) => void
36
  setGeneratingStory: (isGeneratingStory: boolean) => void
 
42
  }>((set, get) => ({
43
  prompt: "",
44
  font: "actionman",
45
+ preset: getRandomPreset(),
46
  nbFrames: 1,
47
  panels: [],
48
+ captions: [],
49
+ showCaptions: false,
50
+ layout: "random",
51
  layouts: getRandomLayoutNames(),
52
  zoomLevel: 60,
53
  page: undefined as unknown as HTMLDivElement,
 
84
  })
85
  },
86
  setPanels: (panels: string[]) => set({ panels }),
87
+ setCaptions: (captions: string[]) => {
88
  set({
89
+ captions,
90
+ })
91
+ },
92
+ setShowCaptions: (showCaptions: boolean) => {
93
+ set({
94
+ showCaptions,
95
  })
96
  },
97
  setLayout: (layoutName: LayoutName) => {
 
130
  const { page } = get()
131
  if (!page) { return "" }
132
 
133
+
134
  const canvas = await html2canvas(page)
135
  console.log("canvas:", canvas)
136
 
 
160
  set({
161
  prompt,
162
  panels: [],
163
+ captions: [],
164
  preset: presetName === "random"
165
  ? getRandomPreset()
166
  : getPreset(presetName),
src/lib/cleanJson.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dirtyLLMResponseCleaner } from "./dirtyLLMResponseCleaner"
2
+
3
+ export function cleanJson(input: string) {
4
+
5
+ let tmp = dirtyLLMResponseCleaner(input)
6
+
7
+ // we only keep what's after the first [
8
+ tmp = `[${tmp.split("[").pop() || ""}`
9
+
10
+ // and before the first ]
11
+ tmp = `${tmp.split("]").shift() || ""}]`
12
+
13
+ tmp = dirtyLLMResponseCleaner(tmp)
14
+
15
+ return tmp
16
+ }
src/lib/dirtyCaptionCleaner.ts CHANGED
@@ -1,3 +1,28 @@
1
- export function dirtyCaptionCleaner(input: string) {
2
- return input.split(":").pop()?.trim() || ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
 
1
+ export function dirtyCaptionCleaner({
2
+ panel,
3
+ instructions,
4
+ caption
5
+ }: {
6
+ panel: number;
7
+ instructions: string;
8
+ caption: string
9
+ }) {
10
+ return {
11
+ panel,
12
+ instructions: (
13
+ // need to remove from LLM garbage here, too
14
+ (instructions.split(":").pop() || "")
15
+ .replaceAll("Show a", "")
16
+ .replaceAll("Show the", "")
17
+ .replaceAll("Opens with a", "")
18
+ .replaceAll("Opens with the", "")
19
+ .replaceAll("Opens with", "")
20
+ .replaceAll("Cut to a", "")
21
+ .replaceAll("Cut to the", "")
22
+ .replaceAll("Cut to", "")
23
+ .replaceAll("End with a", "")
24
+ .replaceAll("End with", "").trim() || ""
25
+ ),
26
+ caption: caption.split(":").pop()?.trim() || "",
27
+ }
28
  }
src/lib/dirtyLLMJsonParser.ts CHANGED
@@ -1,15 +1,28 @@
1
  import { LLMResponse } from "@/types"
 
2
 
3
- export function dirtyLLMJsonParser(input: string): string[] {
4
- // we only keep what's after the first [
5
- let jsonOrNot = `[${input.split("[").pop() || ""}`
6
 
7
- // and before the first ]
8
- jsonOrNot = `${jsonOrNot.split("]").shift() || ""}]`
 
 
 
9
 
10
  const jsonData = JSON.parse(jsonOrNot) as LLMResponse
11
 
12
- const captions = jsonData.map(item => item.caption.trim())
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- return captions
15
  }
 
1
  import { LLMResponse } from "@/types"
2
+ import { cleanJson } from "./cleanJson"
3
 
4
+ export function dirtyLLMJsonParser(input: string): LLMResponse {
 
 
5
 
6
+ if (input.includes("```")) {
7
+ input = input.split("```")[0]
8
+ }
9
+ // we only keep what's after the first [
10
+ let jsonOrNot = cleanJson(input)
11
 
12
  const jsonData = JSON.parse(jsonOrNot) as LLMResponse
13
 
14
+ const results = jsonData.map((item, i) => {
15
+ let panel = i
16
+ let caption = item.caption ? item.caption.trim() : ''
17
+ let instructions = item.instructions ? item.instructions.trim() : ''
18
+ if (!instructions && caption) {
19
+ instructions = caption
20
+ }
21
+ if (!caption && instructions) {
22
+ caption = instructions
23
+ }
24
+ return { panel, caption, instructions }
25
+ })
26
 
27
+ return results
28
  }
src/lib/dirtyLLMResponseCleaner.ts CHANGED
@@ -1,5 +1,5 @@
1
  export function dirtyLLMResponseCleaner(input: string) {
2
- return (
3
  `${input || ""}`
4
  // a summary of all the weird hallucinations I saw it make..
5
  .replaceAll(`"]`, `"}]`)
@@ -10,6 +10,8 @@ export function dirtyLLMResponseCleaner(input: string) {
10
  .replaceAll(`"\n ]`, `"}]`)
11
  .replaceAll("}}", "}")
12
  .replaceAll("]]", "]")
 
 
13
  .replaceAll(",,", ",")
14
  .replaceAll("[0]", "")
15
  .replaceAll("[1]", "")
@@ -22,4 +24,23 @@ export function dirtyLLMResponseCleaner(input: string) {
22
  .replaceAll("[panel 3]", "")
23
  .replaceAll("[panel 4]", "")
24
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
1
  export function dirtyLLMResponseCleaner(input: string) {
2
+ let str = (
3
  `${input || ""}`
4
  // a summary of all the weird hallucinations I saw it make..
5
  .replaceAll(`"]`, `"}]`)
 
10
  .replaceAll(`"\n ]`, `"}]`)
11
  .replaceAll("}}", "}")
12
  .replaceAll("]]", "]")
13
+ .replaceAll("[[", "[")
14
+ .replaceAll("{{", "{")
15
  .replaceAll(",,", ",")
16
  .replaceAll("[0]", "")
17
  .replaceAll("[1]", "")
 
24
  .replaceAll("[panel 3]", "")
25
  .replaceAll("[panel 4]", "")
26
  )
27
+
28
+ // repair missing end of JSON array
29
+ if (str.at(-1) === '}') {
30
+ str = str + "]"
31
+ }
32
+
33
+ if (str.at(-1) === '"') {
34
+ str = str + "}]"
35
+ }
36
+
37
+ if (str[0] === '{') {
38
+ str = "[" + str
39
+ }
40
+
41
+ if (str[0] === '"') {
42
+ str = "[{" + str
43
+ }
44
+
45
+ return str
46
  }
src/types.ts CHANGED
@@ -79,4 +79,4 @@ export interface ImageAnalysisResponse {
79
  error?: string
80
  }
81
 
82
- export type LLMResponse = Array<{panel: number; caption: string }>
 
79
  error?: string
80
  }
81
 
82
+ export type LLMResponse = Array<{panel: number; instructions: string; caption: string }>