+ {#if resultKind === "text" && textResult}
+
+ {})}
+ ondownload={ondownloadtxt ?? (() => {})}
+ />
+
+ {:else if resultKind === "verdict" && textResult}
+
+ {})}
+ ondownload={ondownloadtxt ?? (() => {})}
+ />
+
+ {:else if resultKind === "image" && resultUrl}
{:else if running}
@@ -99,11 +147,25 @@
items={[
{
caption: "SOURCE",
- value: source ? `${source.width} × ${source.height} px` : "—",
+ value:
+ !isGenerator && !isTextInput
+ ? source
+ ? `${source.width} × ${source.height} px`
+ : "—"
+ : isTextInput
+ ? "text"
+ : "—",
},
{
caption: "RESULT",
- value: result ? `${result.width} × ${result.height} px` : "—",
+ value:
+ resultKind === "image"
+ ? result
+ ? `${result.width} × ${result.height} px`
+ : "—"
+ : textResult
+ ? "text"
+ : "—",
},
{ caption: "FORMAT", value: "PNG" },
]}
@@ -202,6 +264,12 @@
.empty {
font: var(--font-size-s) var(--font-mono);
}
+ .text-source-wrap,
+ .text-result-wrap {
+ width: 100%;
+ padding: var(--space-l);
+ box-sizing: border-box;
+ }
.meta {
margin-top: var(--space-l);
}
diff --git a/web/src/lib/components/kit/SchemaTextResult.svelte b/web/src/lib/components/kit/SchemaTextResult.svelte
new file mode 100644
index 0000000..4aa3e64
--- /dev/null
+++ b/web/src/lib/components/kit/SchemaTextResult.svelte
@@ -0,0 +1,129 @@
+
+
+{#if kind === "verdict"}
+
+
RESULT
+
+ {value}
+
+
+
+{:else}
+
+{/if}
+
+
diff --git a/web/src/lib/components/kit/SchemaTextSource.svelte b/web/src/lib/components/kit/SchemaTextSource.svelte
new file mode 100644
index 0000000..f7378ab
--- /dev/null
+++ b/web/src/lib/components/kit/SchemaTextSource.svelte
@@ -0,0 +1,107 @@
+
+
+
+
+
+ {#if onsample}
+
+ Try sample
+
+ {/if}
+
+ Render text
+
+
+
+
+
diff --git a/web/src/lib/components/kit/SchemaToolView.svelte b/web/src/lib/components/kit/SchemaToolView.svelte
index f990f3e..0296fba 100644
--- a/web/src/lib/components/kit/SchemaToolView.svelte
+++ b/web/src/lib/components/kit/SchemaToolView.svelte
@@ -4,7 +4,13 @@
import { debounce } from "$lib/core/debounce";
import { decodeFile, encode } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types";
- import { executeGenerate, executeStep } from "$lib/preview/executor";
+ import {
+ executeFromText,
+ executeGenerate,
+ executeStep,
+ executeTextToText,
+ executeToText,
+ } from "$lib/preview/executor";
import type { ToolEntry } from "$lib/registry-new";
import {
defaultSchemaParams,
@@ -22,10 +28,14 @@
);
const isGenerator = $derived(Boolean(tool.generate && !tool.run));
+ const inputMode = $derived(tool.input ?? "file");
+ const resultKind = $derived(tool.result ?? "image");
let values = $state
>({});
let source = $state(null);
let result = $state(null);
+ let textSource = $state("");
+ let textResult = $state(null);
let running = $state(false);
let error = $state("");
let started = $state(false);
@@ -45,6 +55,7 @@
function reset() {
values = schema ? defaultSchemaParams(schema) : {};
result = null;
+ textResult = null;
error = "";
}
@@ -64,12 +75,29 @@
await runGenerate();
return;
}
- if (!source) return;
error = "";
- running = true;
const params = sanitizeSchemaParams(schema, values);
+ running = true;
try {
- result = await executeStep(tool, source, params);
+ if (inputMode === "text") {
+ if (!textSource.trim()) return;
+ if (tool.textToText) {
+ textResult = await executeTextToText(tool, textSource);
+ result = null;
+ } else {
+ result = await executeFromText(tool, textSource, params);
+ textResult = null;
+ }
+ } else {
+ if (!source) return;
+ if (resultKind !== "image" && tool.toText) {
+ textResult = await executeToText(tool, source, params);
+ result = null;
+ } else {
+ result = await executeStep(tool, source, params);
+ textResult = null;
+ }
+ }
} catch (e) {
error = errorText(e);
} finally {
@@ -106,12 +134,32 @@
URL.revokeObjectURL(url);
}
+ async function copyText() {
+ if (!textResult) return;
+ try {
+ await navigator.clipboard.writeText(textResult);
+ } catch (e) {
+ error = errorText(e);
+ }
+ }
+
+ async function downloadText() {
+ if (!textResult) return;
+ const blob = new Blob([textResult], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${tool.id}.txt`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
$effect(() => {
init();
});
$effect(() => {
- if (isGenerator || !source || !started) return;
+ if (inputMode === "text" || isGenerator || !source || !started) return;
void values;
debouncedRun();
return () => debouncedRun.cancel();
@@ -146,11 +194,20 @@
{
+ textSource = t;
+ }}
+ onrendertext={run}
+ oncopytext={copyText}
+ ondownloadtxt={downloadText}
ondownload={download}
/>
diff --git a/web/src/lib/preview/executor/executor.ts b/web/src/lib/preview/executor/executor.ts
index a7fafe1..a48009a 100644
--- a/web/src/lib/preview/executor/executor.ts
+++ b/web/src/lib/preview/executor/executor.ts
@@ -55,6 +55,53 @@ export async function executeGenerate(
return await tool.generate(params);
}
+type MaybeTextRunnable = MaybeRunnable & {
+ runFromText?: (
+ text: string,
+ params: Record,
+ ) => Promise | PixelImage;
+ toText?: (
+ img: PixelImage,
+ params: Record,
+ ) => Promise | string;
+ textToText?: (text: string) => Promise | string;
+};
+
+/** Текст → изображение (text-source конвертеры). Выполняется напрямую. */
+export async function executeFromText(
+ tool: MaybeTextRunnable,
+ text: string,
+ params: Record,
+): Promise {
+ if (!tool.runFromText) {
+ throw new Error("errors.noTextInput");
+ }
+ return await tool.runFromText(text, params);
+}
+
+/** Изображение → текст (текстовые конвертеры и вердикты). */
+export async function executeToText(
+ tool: MaybeTextRunnable,
+ img: PixelImage,
+ params: Record,
+): Promise {
+ if (!tool.toText) {
+ throw new Error("errors.noTextResult");
+ }
+ return await tool.toText(img, params);
+}
+
+/** Текст → текст (напр. verify-is-png). */
+export async function executeTextToText(
+ tool: MaybeTextRunnable,
+ text: string,
+): Promise {
+ if (!tool.textToText) {
+ throw new Error("errors.noTextResult");
+ }
+ return await tool.textToText(text);
+}
+
async function runDirect(
tool: MaybeRunnable,
img: PixelImage,
diff --git a/web/src/lib/preview/executor/index.ts b/web/src/lib/preview/executor/index.ts
index 02cb028..81e0890 100644
--- a/web/src/lib/preview/executor/index.ts
+++ b/web/src/lib/preview/executor/index.ts
@@ -1 +1,7 @@
-export { executeGenerate, executeStep } from "./executor";
+export {
+ executeFromText,
+ executeGenerate,
+ executeStep,
+ executeTextToText,
+ executeToText,
+} from "./executor";
diff --git a/web/src/lib/registry-new/types.ts b/web/src/lib/registry-new/types.ts
index c261b68..4b5b6f4 100644
--- a/web/src/lib/registry-new/types.ts
+++ b/web/src/lib/registry-new/types.ts
@@ -23,6 +23,10 @@ export type ToolEntry> = {
schema: ToolSchema
;
/** Требует DOM (canvas/document); превью-executor запускает напрямую, не в worker. */
domOnly?: boolean;
+ /** Чем управляется инструмент: файлом (по умолчанию), текстом или ничем (генератор). */
+ input?: "file" | "text" | "none";
+ /** Тип результата: картинка (по умолчанию), большой текст или короткий вердикт. */
+ result?: "image" | "text" | "verdict";
/** Применение к входному изображению. Для чистых генераторов отсутствует. */
run?(img: PixelImage, params: P): Promise | PixelImage;
generate?(params: P): Promise | PixelImage;