@@ -46,15 +46,17 @@ function getStyleSpec(options) {
4646
4747var DEFAULT_PROVIDER_PROMPT_TEMPLATES = {
4848 openai : {
49- storyboard : 'Create a comic storyboard as strict JSON.\nJSON only, no markdown.\nSchema: {"panels":[{"caption":string,"image_prompt":string}]}\nPanels: {{panel_count}}\nStyle: {{style_prompt}}\nContent:\n{{content}}' ,
49+ storyboard : 'Create a comic storyboard as strict JSON.\nJSON only, no markdown.\nSchema: {"panels":[{"caption":string,"image_prompt":string}]}\ncaption must be a short story beat for a reader (graphic-novel narration), not a visual prompt. image_prompt must be visual-generation instructions only.\ nPanels: {{panel_count}}\nStyle: {{style_prompt}}\nContent:\n{{content}}' ,
5050 image : 'Comic panel {{panel_index}}/{{panel_count}}.\nCaption: {{panel_caption}}\nSummary: {{panel_summary}}\nStyle: {{style_prompt}}\nReturn a single image matching the comic style.'
5151 } ,
5252 gemini : {
53- storyboard : 'Generate a comic storyboard in strict JSON.\nJSON only, no markdown.\nSchema: {"panels":[{"caption":string,"image_prompt":string}]}\nPanel count: {{panel_count}}\nStyle guidance: {{style_prompt}}\nContent:\n{{content}}' ,
53+ storyboard : 'Generate a comic storyboard in strict JSON.\nJSON only, no markdown.\nSchema: {"panels":[{"caption":string,"image_prompt":string}]}\ncaption must be a short story beat for a reader (graphic-novel narration), not a visual prompt. image_prompt must be visual-generation instructions only.\ nPanel count: {{panel_count}}\nStyle guidance: {{style_prompt}}\nContent:\n{{content}}' ,
5454 image : 'Create comic panel artwork {{panel_index}}/{{panel_count}}.\nPanel caption: {{panel_caption}}\nPanel summary: {{panel_summary}}\nStyle guidance: {{style_prompt}}'
5555 }
5656} ;
5757var STORYBOARD_RETRY_JSON_ONLY_PROMPT = 'Return ONLY valid JSON object with top-level "panels" array. No markdown fences.' ;
58+ var STORYBOARD_CAPTION_IMAGE_PROMPT_RULE =
59+ 'caption must be a short story beat for a reader (graphic-novel narration), not a visual prompt. image_prompt must be visual-generation instructions only.' ;
5860
5961var DEFAULT_FETCH_TIMEOUT_MS = 45000 ;
6062var STORYBOARD_TIMEOUT_MS = 90000 ;
@@ -388,12 +390,80 @@ function summarizeRawOutputForRetry(error) {
388390 return 'Previous malformed output snippet: "' + snippet + '"' ;
389391}
390392
393+ function looksLikeImagePromptText ( value ) {
394+ var s = normalizeLooseTextValue ( value ) . trim ( ) ;
395+ if ( ! s ) return false ;
396+ var lower = s . toLowerCase ( ) ;
397+ if ( s . length > 220 ) return true ;
398+ var promptPhrases = [
399+ 'comic panel illustration' ,
400+ 'illustration of' ,
401+ 'digital art' ,
402+ 'cinematic lighting' ,
403+ 'highly detailed' ,
404+ 'camera angle' ,
405+ 'art style' ,
406+ 'dramatic lighting' ,
407+ 'ultra detailed'
408+ ] ;
409+ for ( var i = 0 ; i < promptPhrases . length ; i ++ ) {
410+ if ( lower . indexOf ( promptPhrases [ i ] ) >= 0 ) return true ;
411+ }
412+ var commaCount = ( s . match ( / , / g) || [ ] ) . length ;
413+ if ( commaCount >= 6 ) return true ;
414+ return false ;
415+ }
416+
417+ function rewritePromptLikeCaptionToStoryBeat ( captionText , panel , index ) {
418+ var storyCandidates = [
419+ panel && panel . beat_summary ,
420+ panel && panel . summary ,
421+ panel && panel . beat ,
422+ panel && panel . narration ,
423+ panel && panel . description ,
424+ panel && panel . title ,
425+ panel && panel . text ,
426+ panel && panel . text_content ,
427+ panel && panel . caption_text ,
428+ panel && panel . dialogue
429+ ] ;
430+ for ( var i = 0 ; i < storyCandidates . length ; i ++ ) {
431+ var candidate = normalizeLooseTextValue ( storyCandidates [ i ] ) ;
432+ if ( candidate && ! looksLikeImagePromptText ( candidate ) ) return candidate ;
433+ }
434+
435+ // Heuristic fallback: strip common prompt-style boilerplate and visual jargon.
436+ var src = normalizeLooseTextValue ( captionText ) ;
437+ if ( ! src ) return 'Panel ' + ( index + 1 ) ;
438+ var out = src
439+ . replace ( / ^ c o m i c p a n e l i l l u s t r a t i o n o f : \s * / i, '' )
440+ . replace ( / ^ i l l u s t r a t i o n o f : \s * / i, '' )
441+ . replace ( / \b ( d i g i t a l a r t | c i n e m a t i c l i g h t i n g | h i g h l y d e t a i l e d | u l t r a d e t a i l e d | d r a m a t i c l i g h t i n g | c a m e r a a n g l e [ ^ , . ; ] * | a r t s t y l e [ ^ , . ; ] * ) \b / gi, '' )
442+ . replace ( / \s + / g, ' ' )
443+ . replace ( / \s * , \s * , + / g, ', ' )
444+ . replace ( / ^ [ , . \s - ] + | [ , . \s - ] + $ / g, '' ) ;
445+ // Prefer first natural clause/sentence fragment as story beat.
446+ var split = out . split ( / [ . ; ] | , a n d / i) . map ( function ( part ) { return part . trim ( ) ; } ) . filter ( Boolean ) ;
447+ var candidateBeat = split [ 0 ] || out ;
448+ if ( ! candidateBeat ) return 'Panel ' + ( index + 1 ) ;
449+ if ( ! / [ . ! ? ] $ / . test ( candidateBeat ) ) candidateBeat += '.' ;
450+ return candidateBeat . substring ( 0 , 180 ) ;
451+ }
452+
391453function validateStoryboardContract ( storyboard , requestedPanelCount ) {
392454 var normalized = ( storyboard && typeof storyboard === 'object' ) ? storyboard : { } ;
393455 if ( ! Array . isArray ( normalized . panels ) ) normalized . panels = [ ] ;
394456
395457 var beforeMissingCaption = 0 ;
396458 var beforeMissingImagePrompt = 0 ;
459+ var promptLikeCaptionRepairs = 0 ;
460+ var captionQuality = {
461+ totalPanels : 0 ,
462+ nonEmptyCaptions : 0 ,
463+ storyLikeCaptions : 0 ,
464+ promptLikeCaptions : 0 ,
465+ fallbackPanelLabelCaptions : 0
466+ } ;
397467
398468 normalized . panels = normalized . panels
399469 . map ( function ( panel , index ) {
@@ -406,10 +476,27 @@ function validateStoryboardContract(storyboard, requestedPanelCount) {
406476 if ( ! caption ) beforeMissingCaption += 1 ;
407477 if ( ! imagePrompt ) beforeMissingImagePrompt += 1 ;
408478
479+ if ( caption && ( looksLikeImagePromptText ( caption ) || ( imagePrompt && caption === imagePrompt ) ) ) {
480+ var repairedCaption = rewritePromptLikeCaptionToStoryBeat ( caption , p , index ) ;
481+ if ( repairedCaption && repairedCaption !== caption ) {
482+ caption = repairedCaption ;
483+ promptLikeCaptionRepairs += 1 ;
484+ }
485+ }
486+
409487 p . beat_summary = p . beat_summary || beat || '' ;
410488 p . caption = caption || beat || ( 'Panel ' + ( index + 1 ) ) ;
411489 p . image_prompt = imagePrompt || ( 'Comic panel illustration of: ' + p . caption + ( beat ? ( '. ' + beat ) : '' ) ) ;
412490 if ( ! p . panel_id ) p . panel_id = 'panel_' + ( index + 1 ) ;
491+
492+ captionQuality . totalPanels += 1 ;
493+ var finalCaption = normalizeLooseTextValue ( p . caption ) ;
494+ if ( finalCaption ) {
495+ captionQuality . nonEmptyCaptions += 1 ;
496+ if ( looksLikeImagePromptText ( finalCaption ) ) captionQuality . promptLikeCaptions += 1 ;
497+ else captionQuality . storyLikeCaptions += 1 ;
498+ if ( / ^ P a n e l \s + \d + \. ? $ / i. test ( finalCaption ) ) captionQuality . fallbackPanelLabelCaptions += 1 ;
499+ }
413500 return p ;
414501 } )
415502 . filter ( function ( panel ) { return ! ! panel ; } ) ;
@@ -424,7 +511,9 @@ function validateStoryboardContract(storyboard, requestedPanelCount) {
424511 hasPanelsArray : Array . isArray ( ( storyboard && storyboard . panels ) ) || Array . isArray ( normalized . panels ) ,
425512 panelCount : normalized . panels . length ,
426513 missingCaptionBeforeSynthesis : beforeMissingCaption ,
427- missingImagePromptBeforeSynthesis : beforeMissingImagePrompt
514+ missingImagePromptBeforeSynthesis : beforeMissingImagePrompt ,
515+ promptLikeCaptionRepairs : promptLikeCaptionRepairs ,
516+ captionQuality : captionQuality
428517 }
429518 } ;
430519}
@@ -558,6 +647,7 @@ class GeminiProvider {
558647 var prompt = 'Create a ' + panelCount + '-panel comic storyboard.\n' +
559648 'JSON only, no markdown.\n' +
560649 'Schema: {"panels":[{"caption":string,"image_prompt":string}]}\n' +
650+ STORYBOARD_CAPTION_IMAGE_PROMPT_RULE + '\n' +
561651 'Visual style requirement: ' + styleSpec . directive + '\n' +
562652 'Keep the style consistent across all panels.\n' +
563653 'Text: ' + text . substring ( 0 , 4000 ) ;
@@ -687,6 +777,7 @@ class OpenAIProvider {
687777 'Create a ' + ( options . panelCount || 6 ) + '-panel comic storyboard. ' +
688778 'JSON only, no markdown. ' +
689779 'Schema: {"panels":[{"caption":string,"image_prompt":string}]}. ' +
780+ STORYBOARD_CAPTION_IMAGE_PROMPT_RULE + ' ' +
690781 'Style requirement: ' + styleSpec . directive + '. ' +
691782 'Content: ' + text . substring ( 0 , 8000 ) ;
692783 if ( options && options . storyboardTemplate ) {
@@ -723,6 +814,7 @@ class OpenAIProvider {
723814 content :
724815 'You are a comic storyboard generator. Respond with JSON only, no markdown fences. ' +
725816 'Schema: {"panels":[{"caption":string,"image_prompt":string}]}. ' +
817+ STORYBOARD_CAPTION_IMAGE_PROMPT_RULE + ' ' +
726818 'Include the requested art style in each panel image_prompt.'
727819 } ,
728820 {
@@ -909,6 +1001,7 @@ class OpenRouterProvider {
9091001 'Create a comic storyboard from the content below.' ,
9101002 'JSON only, no markdown.' ,
9111003 'Schema: {"panels":[{"caption":string,"image_prompt":string}]}' ,
1004+ STORYBOARD_CAPTION_IMAGE_PROMPT_RULE ,
9121005 'Panel count: ' + panelCount ,
9131006 'Style requirement: ' + styleSpec . directive ,
9141007 'Keep the style consistent across all panels.' ,
@@ -1193,6 +1286,7 @@ class HuggingFaceProvider {
11931286 var prompt = [
11941287 'JSON only, no markdown.' ,
11951288 'Schema: {"panels":[{"caption":string,"image_prompt":string}]}' ,
1289+ STORYBOARD_CAPTION_IMAGE_PROMPT_RULE ,
11961290 'Create a ' + panelCount + '-panel comic storyboard.' ,
11971291 'Style requirement: ' + styleSpec . directive ,
11981292 'Content:' ,
@@ -1513,6 +1607,7 @@ class CloudflareProvider {
15131607 'Create a comic storyboard from the content below.' ,
15141608 'JSON only, no markdown.' ,
15151609 'Schema: {"panels":[{"caption":string,"image_prompt":string}]}' ,
1610+ STORYBOARD_CAPTION_IMAGE_PROMPT_RULE ,
15161611 'Panel count: ' + panelCount ,
15171612 'Style requirement: ' + styleSpec . directive ,
15181613 'Keep image prompts concise but descriptive and safe for image generation.' ,
@@ -1546,6 +1641,7 @@ class CloudflareProvider {
15461641 var data = await this . callModel ( model , {
15471642 messages : [
15481643 { role : 'system' , content : 'You generate comic storyboards as strict JSON only.' } ,
1644+ { role : 'system' , content : STORYBOARD_CAPTION_IMAGE_PROMPT_RULE } ,
15491645 { role : 'user' , content : prompt }
15501646 ] ,
15511647 max_tokens : 2048 ,
@@ -2391,6 +2487,22 @@ var ServiceWorker = function() {
23912487 noPanelsErr . providerId = ( storyboard && storyboard . settings && storyboard . settings . provider_text ) || settings . provider_text ;
23922488 throw noPanelsErr ;
23932489 }
2490+ if ( contract . meta . promptLikeCaptionRepairs > 0 ) {
2491+ self . appendDebugLog ( 'unexpected_output.storyboard.prompt_like_captions_repaired' , {
2492+ count : contract . meta . promptLikeCaptionRepairs ,
2493+ sourceUrl : job . sourceUrl || null ,
2494+ provider : ( storyboard && storyboard . settings && storyboard . settings . provider_text ) || settings . provider_text
2495+ } ) ;
2496+ }
2497+ job . captionQuality = {
2498+ ...( contract . meta . captionQuality || { } ) ,
2499+ promptLikeCaptionRepairs : contract . meta . promptLikeCaptionRepairs || 0
2500+ } ;
2501+ self . appendDebugLog ( 'caption_quality.score' , {
2502+ provider : ( storyboard && storyboard . settings && storyboard . settings . provider_text ) || settings . provider_text ,
2503+ sourceUrl : job . sourceUrl || null ,
2504+ score : job . captionQuality || null
2505+ } ) ;
23942506 storyboard . source = { url : job . sourceUrl , title : job . sourceTitle , extracted_at : new Date ( ) . toISOString ( ) } ;
23952507 storyboard . panels = ( Array . isArray ( storyboard . panels ) ? storyboard . panels : [ ] ) . map ( function ( panel , idx ) {
23962508 var p = panel || { } ;
@@ -2405,6 +2517,7 @@ var ServiceWorker = function() {
24052517 show_rewritten_badge : settings . show_rewritten_badge !== false ,
24062518 log_rewritten_prompts : ! ! settings . log_rewritten_prompts
24072519 } ;
2520+ storyboard . caption_quality = job . captionQuality || null ;
24082521 job . storyboard = storyboard ;
24092522 job . status = 'generating_images' ;
24102523 job . currentPanelIndex = 0 ;
0 commit comments