Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This file is the release-notes source of truth for Marinara Engine. Reuse these

### Added

- Added optional image prompting instructions to image connections, applying them inside existing selfie and Illustrator prompt-writing calls before provider review or generation.
- Added a Gallery image-agent picker that can run any active custom image-producing agent alongside the base Illustrator (#4846).
- Added an editable Storyboard Agent shot-planner stage that inspects each generated keyframe before video generation, persists its suitability classification, and falls back to the planned motion when image-aware refinement is unavailable or invalid. The Storyboard Agent page now explains the four-stage prompt workflow and orders its shared prompt editors from illustration through image-aware grounding to video generation (#4839, Pasta-Devs/Marinara-Agents#296).
- Added batch selection to Character and Persona image galleries so selected images can be downloaded or deleted together after confirmation (#4832).
Expand Down Expand Up @@ -47,6 +48,7 @@ This file is the release-notes source of truth for Marinara Engine. Reuse these

### Fixed

- Scoped image-connection prompt instructions to image-producing agents, bounded stored instructions to 20,000 characters, and kept each retry agent on its own configured image connection.
- Kept chat settings profiles within their saved chat mode, migrated legacy Visual Novel profiles to Roleplay on import, and prevented reusable profiles from overwriting branch identity (#4849).
- Served the Home shell directly for unknown routes so Termux mobile launches cannot recurse through the server's 404 handler (#4850).
- Corrected legacy Google Gemini `/v1` connection URLs to `/v1beta` for connection checks, model discovery, chat generation, and embeddings (#4854).
Expand Down
27 changes: 27 additions & 0 deletions packages/client/src/components/connections/ConnectionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ import {
sanitizeImageGenerationProfile,
sanitizeVideoGenerationProfile,
suggestImageStyleProfileIdForModel,
MAX_IMAGE_PROMPT_INSTRUCTIONS_LENGTH,
normalizeImagePromptInstructions,
parseConnectionImageCaptioningDefaults,
type APIProvider,
type ComfyUiLoraSetting,
Expand Down Expand Up @@ -299,6 +301,7 @@ export function ConnectionEditor() {
const [localComfyuiWorkflow, setLocalComfyuiWorkflow] = useState("");
const [localImageService, setLocalImageService] = useState<string | null>(null);
const [localImageEndpointId, setLocalImageEndpointId] = useState("");
const [localImagePromptInstructions, setLocalImagePromptInstructions] = useState("");
const [localImageGenerationQuality, setLocalImageGenerationQuality] = useState<ImageGenerationQuality>("auto");
const [localVideoGenerationSource, setLocalVideoGenerationSource] = useState("");
const [localVideoService, setLocalVideoService] = useState<string | null>(null);
Expand Down Expand Up @@ -418,6 +421,7 @@ export function ConnectionEditor() {
setLocalComfyuiWorkflow((c.comfyuiWorkflow as string) ?? "");
setLocalImageService(imageService);
setLocalImageEndpointId((c.imageEndpointId as string) ?? "");
setLocalImagePromptInstructions((c.imagePromptInstructions as string) ?? "");
setLocalImageGenerationQuality(
c.imageGenerationQuality === "low" || c.imageGenerationQuality === "medium" || c.imageGenerationQuality === "high"
? c.imageGenerationQuality
Expand Down Expand Up @@ -719,6 +723,7 @@ export function ConnectionEditor() {
imageService: isImageProvider ? localImageGenerationSource || localImageService || null : null,
imageEndpointId:
isImageProvider && selectedImageService === "runpod_comfyui" ? localImageEndpointId || null : null,
imagePromptInstructions: isImageProvider ? normalizeImagePromptInstructions(localImagePromptInstructions) : null,
imageGenerationQuality: isImageProvider ? localImageGenerationQuality : "auto",
videoGenerationSource: isVideoProvider ? selectedVideoProvider || null : null,
videoService: isVideoProvider ? selectedVideoDefaultsService : null,
Expand Down Expand Up @@ -818,6 +823,7 @@ export function ConnectionEditor() {
localComfyuiWorkflow,
localImageService,
localImageEndpointId,
localImagePromptInstructions,
localImageGenerationQuality,
localMaxTokensOverride,
localClaudeFastMode,
Expand Down Expand Up @@ -923,6 +929,7 @@ export function ConnectionEditor() {
videoService,
imageEndpointId:
isImageProvider && selectedImageService === "runpod_comfyui" ? localImageEndpointId || null : null,
imagePromptInstructions: isImageProvider ? normalizeImagePromptInstructions(localImagePromptInstructions) : null,
imageGenerationQuality: isImageProvider ? localImageGenerationQuality : "auto",
comfyuiWorkflow:
isImageProvider || (isVideoProvider && videoProvider === "comfyui") ? localComfyuiWorkflow || null : null,
Expand Down Expand Up @@ -961,6 +968,7 @@ export function ConnectionEditor() {
selectedVideoProvider,
selectedImageService,
localImageEndpointId,
localImagePromptInstructions,
localImageGenerationQuality,
localComfyuiWorkflow,
localClaudeFastMode,
Expand Down Expand Up @@ -2008,6 +2016,25 @@ export function ConnectionEditor() {
</FieldGroup>
)}

{localProvider === "image_generation" && (
<FieldGroup
label={localizeUi("ui.connections.connectioneditor.imagePromptingInstructions")}
icon={<Sparkles size="0.875rem" className="text-sky-400" />}
help={localizeUi("ui.connections.connectioneditor.imagePromptingInstructionsHelp")}
>
<textarea
value={localImagePromptInstructions}
maxLength={MAX_IMAGE_PROMPT_INSTRUCTIONS_LENGTH}
onChange={(event) => {
setLocalImagePromptInstructions(event.target.value);
markDirty();
}}
placeholder={localizeUi("ui.connections.connectioneditor.imagePromptingInstructionsPlaceholder")}
className="w-full min-h-[96px] resize-y rounded-xl bg-[var(--secondary)] px-3 py-2.5 text-sm outline-none ring-1 ring-[var(--border)] transition-shadow placeholder:text-[var(--muted-foreground)]/50 focus:ring-sky-400/50"
/>
</FieldGroup>
)}

{supportsGptImageQuality && (
<FieldGroup
label={localizeUi("ui.connections.connectioneditor.gptImageQuality")}
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/hooks/use-connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type CreateConnectionPayload = {
comfyuiWorkflow?: string | null;
imageService?: string | null;
imageEndpointId?: string | null;
imagePromptInstructions?: string | null;
imageGenerationQuality?: ImageGenerationQuality;
videoGenerationSource?: string | null;
videoService?: string | null;
Expand Down
11 changes: 10 additions & 1 deletion packages/client/src/lib/connection-transfer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { PROVIDERS, type APIProvider, type ImageGenerationQuality } from "@marinara-engine/shared";
import {
normalizeImagePromptInstructions,
PROVIDERS,
type APIProvider,
type ImageGenerationQuality,
} from "@marinara-engine/shared";
import type { CreateConnectionPayload } from "../hooks/use-connections";

export type ConnectionTransferRow = {
Expand Down Expand Up @@ -29,6 +34,7 @@ export type ConnectionTransferRow = {
videoService?: unknown;
service?: unknown;
imageEndpointId?: unknown;
imagePromptInstructions?: unknown;
imageGenerationQuality?: unknown;
comfyuiWorkflow?: unknown;
treatAsLocalEndpoint?: unknown;
Expand Down Expand Up @@ -62,6 +68,7 @@ export type SafeConnectionExport = {
videoGenerationSource: string | null;
videoService: string | null;
imageEndpointId: string | null;
imagePromptInstructions: string | null;
imageGenerationQuality: ImageGenerationQuality;
comfyuiWorkflow: string | null;
treatAsLocalEndpoint: boolean;
Expand Down Expand Up @@ -133,6 +140,7 @@ export function normalizeImportedConnectionEntry(value: unknown): ConnectionImpo
comfyuiWorkflow: asNullableString(value.comfyuiWorkflow),
imageService,
imageEndpointId: asNullableString(value.imageEndpointId),
imagePromptInstructions: normalizeImagePromptInstructions(value.imagePromptInstructions),
imageGenerationQuality: asImageGenerationQuality(value.imageGenerationQuality),
videoGenerationSource: provider === "video_generation" ? asNullableString(value.videoGenerationSource) : null,
videoService,
Expand Down Expand Up @@ -177,6 +185,7 @@ function serializeConnectionForExport(connection: ConnectionTransferRow): SafeCo
videoGenerationSource: isVideoProvider ? asNullableString(connection.videoGenerationSource) : null,
videoService: isVideoProvider ? asNullableString(connection.videoService ?? connection.service) : null,
imageEndpointId: asNullableString(connection.imageEndpointId),
imagePromptInstructions: normalizeImagePromptInstructions(connection.imagePromptInstructions),
imageGenerationQuality: asImageGenerationQuality(connection.imageGenerationQuality),
comfyuiWorkflow: asNullableString(connection.comfyuiWorkflow),
treatAsLocalEndpoint: asBoolean(connection.treatAsLocalEndpoint),
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/localization/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4082,6 +4082,9 @@
"ui.connections.connectioneditor.httpsUsCentral1AiplatformGoogleapisComV1ProjectsMy": "https://us-central1-aiplatform.googleapis.com/v1/projects/my-project/locations/us-central1",
"ui.connections.connectioneditor.ifYourProxyOrLocalServerIsnTDetected": "If your proxy or local server isn't detected, Windows Defender Firewall may be blocking the connection. Open",
"ui.connections.connectioneditor.imageCaptioning": "Image Captioning",
"ui.connections.connectioneditor.imagePromptingInstructions": "Image Prompting Instructions",
"ui.connections.connectioneditor.imagePromptingInstructionsHelp": "When set, these instructions are added to the existing prompt-writer system message and processed by the configured Illustrator provider and model in selfie and Illustrator flows before the prompt is sent to this image backend. Useful for tags, quality tokens, negative-prompt conventions, or local/ComfyUI syntax. An invalid-JSON retry may trigger another call.",
"ui.connections.connectioneditor.imagePromptingInstructionsPlaceholder": "Example: use comma-separated tags, add quality tokens, and keep negative terms separate.",
Comment thread
snpNEXT marked this conversation as resolved.
"ui.connections.connectioneditor.imageQualityAuto": "Auto",
"ui.connections.connectioneditor.imageQualityHigh": "High",
"ui.connections.connectioneditor.imageQualityLow": "Low",
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/db/schema/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export const apiConnections = fileTable("api_connections", {
imageService: text("image_service"),
/** For endpoint-based image services (e.g. RunPod Serverless ComfyUI): the endpoint ID. */
imageEndpointId: text("image_endpoint_id"),
/** Instructions passed to the default language model before image generation. */
imagePromptInstructions: text("image_prompt_instructions"),
/** OpenAI GPT Image quality for this connection. */
imageGenerationQuality: text("image_generation_quality").notNull().default("auto"),
/** Explicit video backend selection for video-generation connections. */
Expand Down
9 changes: 7 additions & 2 deletions packages/server/src/routes/gallery.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
import { resolveIllustratorPromptRuntime } from "../services/generation/illustrator-prompt-runtime.js";
import { resolveIllustratorImageConnectionId } from "../services/generation/illustrator-background-generation.js";
import { resolveConversationSelfieSystemPrompt } from "../services/conversation/selfie-prompt.js";
import { appendImagePromptInstructions } from "../services/generation/image-prompt-instructions.js";
import {
suppressesReferencePromptLine,
resolveIllustratorCharacterReferences,
Expand Down Expand Up @@ -1255,6 +1256,10 @@ export async function galleryRoutes(app: FastifyInstance) {
const selfieSystemPrompt = styleGuidance
? `${baseSelfieSystemPrompt}${formatImageStylePromptGuidance(styleGuidance)}`
: baseSelfieSystemPrompt;
const selfieSystemPromptWithImageInstructions = appendImagePromptInstructions(
selfieSystemPrompt,
imageConn.imagePromptInstructions,
);

const selfieAbortSignal = createResponseAbortSignal(reply, SCENE_VIDEO_GENERATION_TIMEOUT_MS, "Selfie generation");
let promptRuntime;
Expand All @@ -1276,7 +1281,7 @@ export async function galleryRoutes(app: FastifyInstance) {
: `Generate a casual selfie of ${characterName} based on the current conversation context.`;

if (debugLogsEnabled) {
debugLog("[debug/gallery/selfie] prompt-builder system:\n%s", selfieSystemPrompt);
debugLog("[debug/gallery/selfie] prompt-builder system:\n%s", selfieSystemPromptWithImageInstructions);
debugLog("[debug/gallery/selfie] prompt-builder user:\n%s", promptContext);
}

Expand All @@ -1285,7 +1290,7 @@ export async function galleryRoutes(app: FastifyInstance) {
try {
const promptResult = await promptBuilder.chatComplete(
[
{ role: "system", content: selfieSystemPrompt },
{ role: "system", content: selfieSystemPromptWithImageInstructions },
{ role: "user", content: promptContext },
],
{
Expand Down
9 changes: 5 additions & 4 deletions packages/server/src/routes/game.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { listPartySprites, readPreferredFullBodySpriteBase64 } from "../services
import {
buildSceneAnalyzerSystemPrompt,
buildSceneAnalyzerUserPrompt,
compactImagePromptInstructions,
type SceneAnalyzerContext,
} from "../services/sidecar/scene-analyzer.js";
import { postProcessSceneResult, type PostProcessContext } from "../services/sidecar/scene-postprocess.js";
Expand Down Expand Up @@ -10316,7 +10317,7 @@ export async function gameRoutes(app: FastifyInstance) {
.catch(() => null);
const imagePromptInstructions =
typeof meta.gameImagePromptInstructions === "string"
? meta.gameImagePromptInstructions.trim().slice(0, 5000)
? compactImagePromptInstructions(meta.gameImagePromptInstructions)
: "";

// Compute approximate turn number: count user messages + 1 (current turn)
Expand Down Expand Up @@ -11199,7 +11200,7 @@ export async function gameRoutes(app: FastifyInstance) {
null;
const imagePromptInstructions =
ownerMode === "game" && typeof meta.gameImagePromptInstructions === "string"
? meta.gameImagePromptInstructions.trim().slice(0, 5000)
? compactImagePromptInstructions(meta.gameImagePromptInstructions)
: "";
const useAvatarReferences = meta.storyboardAgentUseAvatarReferences !== false;
const useStoryboardPromptTemplate =
Expand Down Expand Up @@ -12153,7 +12154,7 @@ export async function gameRoutes(app: FastifyInstance) {
null;
const imagePromptInstructions =
typeof meta.gameImagePromptInstructions === "string"
? meta.gameImagePromptInstructions.trim().slice(0, 5000)
? compactImagePromptInstructions(meta.gameImagePromptInstructions)
: "";
const useAvatarReferences = input.useAvatarReferences ?? meta.gameImageUseAvatarReferences !== false;
const includeCharacterAppearance =
Expand Down Expand Up @@ -12566,7 +12567,7 @@ export async function gameRoutes(app: FastifyInstance) {
null;
const imagePromptInstructions =
typeof meta.gameImagePromptInstructions === "string"
? meta.gameImagePromptInstructions.trim().slice(0, 5000)
? compactImagePromptInstructions(meta.gameImagePromptInstructions)
: "";
const useAvatarReferences = input.useAvatarReferences ?? meta.gameImageUseAvatarReferences !== false;
const includeCharacterAppearance =
Expand Down
38 changes: 35 additions & 3 deletions packages/server/src/routes/generate.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
unwrapConversationInstructions,
findKnownModel,
LOCAL_SIDECAR_CONNECTION_ID,
normalizeImagePromptInstructions,
normalizeTextForMatch,
parseManagedGenerationParameterDefinitions,
normalizeGameStoryboardKeyframeCount,
Expand Down Expand Up @@ -157,7 +158,12 @@ import {
import { executeToolCalls, formatToolExecutionResultForModel } from "../services/tools/tool-executor.js";
import { createAgentPipeline, type ResolvedAgent, type AgentInjection } from "../services/agents/agent-pipeline.js";
import { DATA_DIR } from "../utils/data-dir.js";
import { executeAgent, normalizeAgentContextSize, resolveAgentResultType } from "../services/agents/agent-executor.js";
import {
executeAgent,
normalizeAgentContextSize,
resolveAgentResultType,
type AgentExecConfig,
} from "../services/agents/agent-executor.js";
import { matchCustomAgentActivation } from "./generate/agent-activation.js";
import { listCharacterSprites } from "../services/game/sprite.service.js";
import {
Expand Down Expand Up @@ -4336,7 +4342,32 @@ export async function generateRoutes(app: FastifyInstance) {
// Pre-generation prompt-patch agents read the assembled prompt here; this is overwritten
// with the fitted provider prompt before each main model call.
agentContext.memory._mainPromptPreview = promptPreviewForAgents(finalMessages);
const pipeline = createAgentPipeline(pipelineAgents, agentContext, sendAgentEventAfterMainStream);
const resolveImagePromptAgentContext = async (agent: AgentExecConfig, context: AgentContext): Promise<AgentContext> => {
const isImagePromptAgent =
agent.type === "illustrator" ||
(agent.isCustomAgent === true && customAgentHasCapability(agent.settings, "trigger_image_generation"));
if (!isImagePromptAgent) return context;

const memory = { ...context.memory };
delete memory._imagePromptInstructions;
const imageConnectionId =
agent.type === "illustrator"
? resolveIllustratorImageConnectionId(chatMode, chatMeta, agent.settings.imageConnectionId)
: typeof agent.settings.imageConnectionId === "string"
? agent.settings.imageConnectionId.trim()
: "";
let imageConnection = imageConnectionId ? await connections.getWithKey(imageConnectionId) : null;
imageConnection ??= await connections.getDefaultForImageGeneration();
const imagePromptInstructions = normalizeImagePromptInstructions(imageConnection?.imagePromptInstructions);
if (imagePromptInstructions) memory._imagePromptInstructions = imagePromptInstructions;
return { ...context, memory };
};
const pipeline = createAgentPipeline(
pipelineAgents,
agentContext,
sendAgentEventAfterMainStream,
resolveImagePromptAgentContext,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let directorSecretPlotResults: AgentResult[] = [];
let directorSecretPlotArcForPrompt: unknown = directorSecretPlotMemory.overarchingArc;

Expand Down Expand Up @@ -7622,9 +7653,10 @@ export async function generateRoutes(app: FastifyInstance) {
historicalLorebookTarget.id,
) ?? phaseRetryContext)
: phaseRetryContext;
const resolvedRetryContext = await resolveImagePromptAgentContext(agentCfg, retryCtx);
const retried = await executeAgent(
agentCfg,
retryCtx,
resolvedRetryContext,
agentCfg.provider,
agentCfg.model,
agentCfg.type === "spotify" ? undefined : agentCfg.toolContext,
Expand Down
Loading
Loading