mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
fix broken automatic metadata parsing
This commit is contained in:
@@ -152,8 +152,6 @@ export function VideoGenerationForm({ engine }: { engine: OrchestratorEngine2 })
|
||||
graphInput.model = model;
|
||||
graphInput.resources = splitResources;
|
||||
|
||||
console.log({ graphInput });
|
||||
|
||||
// Clone generation graph, initialize with mapped data, and validate
|
||||
const graph = generationGraph.clone();
|
||||
const externalCtx = buildGraphContext(status);
|
||||
@@ -169,8 +167,6 @@ export function VideoGenerationForm({ engine }: { engine: OrchestratorEngine2 })
|
||||
|
||||
const graphData = result.data;
|
||||
|
||||
console.log({ graphData });
|
||||
|
||||
conditionalPerformTransaction(cost, () => {
|
||||
mutate({
|
||||
input: graphData,
|
||||
@@ -363,7 +359,6 @@ function SubmitButton2({
|
||||
: [];
|
||||
|
||||
const { model, resources: splitResources } = splitResourcesByType(parsedResources);
|
||||
console.log({ parsedResources, model, splitResources });
|
||||
graphInput.model = model;
|
||||
graphInput.resources = splitResources;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import { usePromptFocusedStore } from '../inputs/PromptInput';
|
||||
* Changes to these fields will NOT trigger a new whatIf query.
|
||||
* Note: 'prompt' IS included because it affects SFW/NSFW classification and pricing.
|
||||
*/
|
||||
const IGNORED_KEYS_FOR_WHATIF = ['negativePrompt', 'seed', 'denoise'] as const;
|
||||
const IGNORED_KEYS_FOR_WHATIF = ['negativePrompt', 'seed'] as const;
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
|
||||
@@ -11,6 +11,8 @@ import { flux2KleinSampleMethods } from '~/shared/orchestrator/ImageGen/flux2-kl
|
||||
import { zImageSampleMethods } from '~/shared/orchestrator/ImageGen/zImage.config';
|
||||
import { defaultCatch } from '~/utils/zod-helpers';
|
||||
|
||||
const schedulers = ['simple', 'discrete', 'karras', 'exponential', 'ays'] as const;
|
||||
|
||||
// All valid samplers: UI samplers + sdcpp samplers for ZImageBase/Flux2Klein
|
||||
const allValidSamplers = [
|
||||
...generationSamplers,
|
||||
@@ -35,7 +37,12 @@ export const textToImageParamsSchema = z.object({
|
||||
sampler: z.string().refine((val) => allValidSamplers.includes(val as any), {
|
||||
error: 'Invalid sampler',
|
||||
}),
|
||||
scheduler: z.enum(['simple', 'discrete', 'karras', 'exponential', 'ays']).optional(),
|
||||
scheduler: z
|
||||
.string()
|
||||
.transform((val) =>
|
||||
['simple', 'discrete', 'karras', 'exponential', 'ays'].includes(val) ? val : 'simple'
|
||||
)
|
||||
.optional(),
|
||||
seed: z.coerce.number().min(1).max(generation.maxValues.seed).nullish().catch(null),
|
||||
clipSkip: z.coerce.number().optional(),
|
||||
steps: z.coerce.number().min(1).max(100).optional(),
|
||||
|
||||
@@ -385,7 +385,7 @@ export const klingGraph = new DataGraph<KlingCtx, GenerationCtx>()
|
||||
.merge(
|
||||
createCheckpointGraph({
|
||||
versions: { options: klingVersionOptions },
|
||||
defaultModelId: klingVersionIds.v3,
|
||||
defaultModelId: klingVersionIds.v2_5_turbo, // Default to V2.5 Turbo for best performance/quality balance
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -111,11 +111,11 @@ export const stableDiffusionGraph = new DataGraph<
|
||||
sliderNode({
|
||||
min: 10,
|
||||
max: 50,
|
||||
defaultValue: 25,
|
||||
defaultValue: 30,
|
||||
presets: [
|
||||
{ label: 'Fast', value: 15 },
|
||||
{ label: 'Balanced', value: 25 },
|
||||
{ label: 'High', value: 35 },
|
||||
{ label: 'Fast', value: 20 },
|
||||
{ label: 'Balanced', value: 30 },
|
||||
{ label: 'High', value: 40 },
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ExifParser } from '~/utils/metadata';
|
||||
import { automaticMetadataProcessor } from '~/utils/metadata/automatic.metadata';
|
||||
|
||||
// TODO - create a suite of tests that uses images from civitai to test the different metadata parsers
|
||||
|
||||
const testImageUrl =
|
||||
'https://orchestration-new.civitai.com/v2/consumer/blobs/E8S6FBPH50ENNVF2PD5XRBPXB0.jpeg?sig=CfDJ8N_qP_UguotCoWxV0GDJyrqO3glAlXP7D6Hb6iXbU9BAZ6no3Pzo0PfuPoI42wFTr-BalGCgmLC5CRJMpYU8Rpi_QqslJAMyMrcmNmeutDpUiZ-C8oAhuumEznFYdbOq3d1hQK4mW0qylzz7qmpPnTxZfJ67zOPQUvskp1Fwv_Xoh_1wKej0bllrUzLdAgpnOVnrb9LTQC8yjeYn2SRQPOFx5pYwna3qVSIXh1Oz-TN7yISHZEncedPwDrwyN91iwRqWujthjJHfyJ_ziykqJCdXmyi5mzp1fp5pzRTRjWOx&exp=2027-02-26T17:34:23.6860202Z';
|
||||
|
||||
// The actual generationDetails string extracted from the test image.
|
||||
// The Civitai metadata contains nested objects (aspectRatio, resources array).
|
||||
const realGenerationDetails = `an ancient warrior princess with sad face, from the side, looking up, in rain, a small stream of water running down over her face, high contrast shadowing,
|
||||
candid style. high contrast, grain effect prominent throughout image, high contrast lighting creating dramatic shadows, grainy film-like texture, nipples
|
||||
Negative prompt: photo , photography, bad quality, bad anatomy, worst quality, low quality, low resolution, extra fingers, blur, blurry, ugly, wrong proportions, watermark, image artifacts, lowres, ugly, jpeg artifacts, deformed, noisy image
|
||||
Steps: 40, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2027225909, Size: 1216x832, Clip skip: 2, Created Date: 2026-02-25T22:09:08.8166925Z, Civitai resources: [{"type":"checkpoint","modelVersionId":1714314,"modelName":"Plant Milk \\uD83C\\uDF3F - Model Suite","modelVersionName":"Hemp II"}], Civitai metadata: {"workflow":"txt2img","output":"image","input":"text","priority":"low","outputFormat":"jpeg","ecosystem":"Illustrious","quantity":4,"aspectRatio":{"value":"3:2","width":1216,"height":832},"negativePrompt":"photo , photography, bad quality, bad anatomy, worst quality, low quality, low resolution, extra fingers, blur, blurry, ugly, wrong proportions, watermark, image artifacts, lowres, ugly, jpeg artifacts, deformed, noisy image","sampler":"DPM++ 2M Karras","cfgScale":7,"steps":40,"clipSkip":2,"seed":2027225909,"enhancedCompatibility":false,"prompt":"an ancient warrior princess with sad face, from the side, looking up, in rain, a small stream of water running down over her face, high contrast shadowing,\\n candid style. high contrast, grain effect prominent throughout image, high contrast lighting creating dramatic shadows, grainy film-like texture, nipples","resources":[{"modelVersionId":1714314,"strength":1,"type":"Checkpoint"}]}`;
|
||||
|
||||
describe('ExifParser - test image URL', () => {
|
||||
it('should parse metadata from the test image without error', async () => {
|
||||
const parser = await ExifParser(testImageUrl);
|
||||
const parsed = parser.parse();
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed?.prompt).toContain('ancient warrior princess');
|
||||
expect(parsed?.extra).toBeDefined();
|
||||
expect(parsed?.extra).toHaveProperty('workflow', 'txt2img');
|
||||
expect(parsed?.extra).toHaveProperty('ecosystem', 'Illustrious');
|
||||
});
|
||||
});
|
||||
|
||||
describe('automaticMetadataProcessor - Civitai metadata with nested JSON', () => {
|
||||
it('parses Civitai metadata with nested objects from real image data', () => {
|
||||
const exif = { generationDetails: realGenerationDetails, parameters: realGenerationDetails };
|
||||
|
||||
expect(automaticMetadataProcessor.canParse(exif)).toBe(true);
|
||||
|
||||
const result = automaticMetadataProcessor.parse(exif);
|
||||
expect(result.extra).toBeDefined();
|
||||
expect(result.extra).toHaveProperty('workflow', 'txt2img');
|
||||
expect(result.extra).toHaveProperty('ecosystem', 'Illustrious');
|
||||
expect(result.extra?.aspectRatio).toEqual({ value: '3:2', width: 1216, height: 832 });
|
||||
expect(result.extra?.resources).toEqual([
|
||||
{ modelVersionId: 1714314, strength: 1, type: 'Checkpoint' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses Civitai metadata with nested objects (minimal case)', () => {
|
||||
const metadata = `Steps: 20, Sampler: Euler, Civitai metadata: {"flat": "ok", "nested": {"inner": "value"}}`;
|
||||
const exif = { generationDetails: metadata, parameters: metadata };
|
||||
|
||||
expect(automaticMetadataProcessor.canParse(exif)).toBe(true);
|
||||
|
||||
const result = automaticMetadataProcessor.parse(exif);
|
||||
expect(result.extra).toEqual({ flat: 'ok', nested: { inner: 'value' } });
|
||||
});
|
||||
|
||||
it('parses flat Civitai metadata', () => {
|
||||
const metadata = `Steps: 20, Sampler: Euler, Civitai metadata: {"remixOfId": 123, "workflow": "txt2img"}`;
|
||||
const exif = { generationDetails: metadata, parameters: metadata };
|
||||
|
||||
expect(automaticMetadataProcessor.canParse(exif)).toBe(true);
|
||||
|
||||
const result = automaticMetadataProcessor.parse(exif);
|
||||
expect(result.extra).toEqual({ remixOfId: 123, workflow: 'txt2img' });
|
||||
});
|
||||
|
||||
it('does not leave Civitai metadata fragments in other parsed fields', () => {
|
||||
const metadata = `Steps: 20, Sampler: Euler, Size: 512x512, Civitai metadata: {"workflow": "txt2img", "nested": {"a": 1}}`;
|
||||
const exif = { generationDetails: metadata, parameters: metadata };
|
||||
|
||||
const result = automaticMetadataProcessor.parse(exif);
|
||||
expect(result.steps).toBe('20');
|
||||
expect(result.sampler).toBe('Euler');
|
||||
expect(result['Size']).toBe('512x512');
|
||||
// Civitai metadata should be fully removed from details line, not leaking into other fields
|
||||
expect(result['Civitai metadata']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ type CivitaiResource = {
|
||||
// #region [helpers]
|
||||
const hashesRegex = /, Hashes:\s*({[^}]+})/;
|
||||
const civitaiResources = /, Civitai resources:\s*(\[\{.*?\}\])/;
|
||||
const civitaiMetadata = /, Civitai metadata:\s*(\{.*?\})/;
|
||||
const civitaiMetadataPrefix = ', Civitai metadata: ';
|
||||
const badExtensionKeys = ['Resources: ', 'Hashed prompt: ', 'Hashed Negative prompt: '];
|
||||
const templateKeys = ['Template: ', 'Negative Template: '] as const;
|
||||
const automaticExtraNetsRegex = /<(lora|hypernet):([a-zA-Z0-9_\.\-]+):([0-9.]+)>/g;
|
||||
@@ -47,6 +47,48 @@ const excludedKeys = [
|
||||
'other',
|
||||
'external',
|
||||
];
|
||||
/** Extract a balanced JSON object from a string, handling nested braces. */
|
||||
function extractBalancedJson(
|
||||
str: string,
|
||||
prefix: string
|
||||
): { json: string; start: number; end: number } | null {
|
||||
const prefixIndex = str.indexOf(prefix);
|
||||
if (prefixIndex === -1) return null;
|
||||
|
||||
const jsonStart = str.indexOf('{', prefixIndex + prefix.length);
|
||||
if (jsonStart === -1) return null;
|
||||
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escape = false;
|
||||
|
||||
for (let i = jsonStart; i < str.length; i++) {
|
||||
const char = str[i];
|
||||
if (escape) {
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if (char === '\\') {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
if (inString) continue;
|
||||
if (char === '{') depth++;
|
||||
else if (char === '}') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
return { json: str.substring(jsonStart, i + 1), start: prefixIndex, end: i + 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPartialDate(date: string) {
|
||||
return date.length === 14 && date[11] === 'T';
|
||||
}
|
||||
@@ -158,12 +200,16 @@ export const automaticMetadataProcessor = createMetadataProcessor({
|
||||
detailsLine = detailsLine.replace(civitaiResources, '');
|
||||
}
|
||||
|
||||
// Extract Civitai Metadata
|
||||
const civitaiMetadataMatch = detailsLine?.match(civitaiMetadata)?.[1];
|
||||
// Extract Civitai Metadata (uses balanced brace extraction to handle nested JSON)
|
||||
const civitaiMetadataMatch = detailsLine
|
||||
? extractBalancedJson(detailsLine, civitaiMetadataPrefix)
|
||||
: null;
|
||||
if (civitaiMetadataMatch && detailsLine) {
|
||||
const data = JSON.parse(civitaiMetadataMatch) as Record<string, any>;
|
||||
const data = JSON.parse(civitaiMetadataMatch.json) as Record<string, any>;
|
||||
if (Object.keys(data).length !== 0) metadata.extra = data;
|
||||
detailsLine = detailsLine.replace(civitaiMetadata, '');
|
||||
detailsLine =
|
||||
detailsLine.slice(0, civitaiMetadataMatch.start) +
|
||||
detailsLine.slice(civitaiMetadataMatch.end);
|
||||
}
|
||||
|
||||
// Extract fine details
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@
|
||||
"./node_modules",
|
||||
"./node_modules/*",
|
||||
"src/pages/api/dev-local/*.ts",
|
||||
"scripts/local-dev/*.ts"
|
||||
"scripts/local-dev/*.ts",
|
||||
"src/**/__tests__/**"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user