mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 23:21:25 +02:00
chore(mana): comic aus unified-App entfernen
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
Comic-Surface ist nach Comicello (comicello.mana.how + comicello.com)
umgezogen. Comicello hat seit Phase ω-2 (2026-05-18) Feature-Parität
+ Cross-Module-Hooks via mana-share:
- Character-Variant-Render mit pinned-Variant-Anker
- Panel-Render via mana-image-edits
- Panel-Edit/Delete/Reorder
- POST /api/v1/share/receive für externe Apps (Journal/Notes/Calendar/
Library/Writing können Text-Snippet rüberschicken, Story wird als
Draft angelegt)
- /comics/new?text=…&sourceModule=… URL-Prefill als Alternative zum
Server-Roundtrip
Gelöscht / abgebaut:
- Module: apps/mana/.../modules/comic + Routen + Locales
- Backend: apps/api/src/modules/comic, picture/routes
verifyMediaOwnership-Allowlist auf nur `['me']` reduziert (comic-
Tag war hier Identity-Anchor-Quelle für panel renders, jetzt
Comicello-intern)
- shared-branding: APP_ICONS.comic + MANA_APPS comic-Entry
- shared-ai/tools/schemas: ganzer Comic-Block (list_comic_stories,
create_comic_story, generate_comic_panel, list_comic_characters,
create_comic_character, generate_character_variant,
pin_character_variant)
- shared-ai/agents/templates: comic-author.ts + index.ts Eintrag
- mana-tool-registry: modules/comic.ts + types ModuleId 'comic' raus
- Cross-Module: website/embeds resolveComicStories +
LocalComicStory-Import + 'comic.stories' EmbedSource + Inspector-
Option; crypto-registry comicStories+comicCharacters; exposed-records
comic-Eintrag
- picture/types: comicStoryId, comicPanelIndex, comicCharacterId
Back-Ref-Felder (sowohl LocalImage als auch Image-Public-DTO);
picture/queries to-Public-Mapping
- Registries: app-registry/apps.ts (Comic registerApp + FilmStrip-
Icon + Header), categories, help-content, module-registry,
data/tools/init
- i18n: comic in apps/{de,en,es,fr,it}.json
Was BLEIBT:
- cloudflared `comicello.mana.how` + `comicello-api.mana.how` —
Standalone-Routes
- docker-compose mana-auth CORS_ORIGINS comicello.com + .mana.how —
SSO für Standalone
Dexie v66:
- droppt comicStories + comicCharacters
- Upgrade-Callback strippt comicStoryId/comicPanelIndex/comicCharacterId
aus existierenden Image-Rows (waren nie indiziert, nur Properties)
mana-web svelte-check 0/0 (7281 files), snapshot test 10/10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c13308cf4
commit
2b08e2f3a2
73 changed files with 36 additions and 8519 deletions
|
|
@ -39,7 +39,6 @@ import { articlesRoutes } from './modules/articles/routes';
|
|||
import { startArticleImportWorker } from './modules/articles/import-worker';
|
||||
import { tracesRoutes } from './modules/traces/routes';
|
||||
import { writingRoutes } from './modules/writing/routes';
|
||||
import { comicRoutes } from './modules/comic/routes';
|
||||
import { presiRoutes } from './modules/presi/routes';
|
||||
import { researchRoutes } from './modules/research/routes';
|
||||
import { websiteRoutes } from './modules/website/routes';
|
||||
|
|
@ -137,7 +136,6 @@ app.route('/api/v1/research', researchRoutes);
|
|||
app.route('/api/v1/website', websiteRoutes);
|
||||
app.route('/api/v1/unlisted', unlistedRoutes);
|
||||
app.route('/api/v1/writing', writingRoutes);
|
||||
app.route('/api/v1/comic', comicRoutes);
|
||||
app.route('/api/v1/personas/admin', personasAdminRoutes);
|
||||
|
||||
// ─── Background Workers ─────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -91,10 +91,9 @@ export async function getMediaBufferAsPng(
|
|||
* doesn't land in the owned set — the caller turns that into an HTTP
|
||||
* response.
|
||||
*
|
||||
* Accepts a single app string or an array. Comic character-ref flows
|
||||
* pass `['me', 'comic']` in one call when both face/body portraits and
|
||||
* comic-specific anchors are legitimate inputs for the same
|
||||
* `/v1/images/edits` POST.
|
||||
* Accepts a single app string or an array. `['me']` covers the
|
||||
* portrait flow; future apps may extend the list with their own
|
||||
* upload tags.
|
||||
*
|
||||
* One `list()` round-trip per app. For N apps this is N calls, each
|
||||
* capped at 500 rows — far beyond the product's intended per-app shape
|
||||
|
|
|
|||
|
|
@ -1,217 +0,0 @@
|
|||
/**
|
||||
* Comic module — server endpoints.
|
||||
*
|
||||
* Current scope (M4):
|
||||
* - POST /storyboard — one-shot panel-sequence suggestion from a text
|
||||
* input (journal entry, note, library review, writing draft,
|
||||
* calendar event description). The client decrypts the source
|
||||
* locally, sends the plaintext + style, and we round-trip to
|
||||
* mana-llm with a JSON-schema system prompt, returning
|
||||
* `{ panels: Array<{ prompt, caption?, dialogue? }> }`. Panel
|
||||
* rendering itself still happens through /picture/generate-with-
|
||||
* reference — this endpoint is pure text → plan.
|
||||
*
|
||||
* Future (M5+):
|
||||
* - Upload endpoint for comic-specific anchor / backdrop images if
|
||||
* M6 character-cast scope happens; the 'comic' upload slot is
|
||||
* already allowed by verifyMediaOwnership (set in M1).
|
||||
*
|
||||
* Why not reuse /api/v1/writing/generations?
|
||||
* That endpoint is a free-text prose endpoint (no JSON parsing) and
|
||||
* is wired for one-shot writing drafts. Comic storyboarding wants a
|
||||
* structured Panel[] envelope the client can iterate over cheaply —
|
||||
* different prompt shape, different parser, different observability
|
||||
* tag. Keeping them apart avoids prompt-contamination between the
|
||||
* two use-cases and keeps each module's logs grep-able.
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { llmJson, LlmError } from '../../lib/llm';
|
||||
import { MANA_LLM } from '@mana/shared-ai';
|
||||
import { logger, type AuthVariables } from '@mana/shared-hono';
|
||||
|
||||
const STORYBOARD_MODEL = MANA_LLM.STRUCTURED;
|
||||
|
||||
type ComicStyle = 'comic' | 'manga' | 'cartoon' | 'graphic-novel' | 'webtoon';
|
||||
|
||||
const STYLE_HINTS: Record<ComicStyle, string> = {
|
||||
comic: 'US comic book, bold linework, cell-shading, dramatic framing',
|
||||
manga: 'Japanese manga, black-and-white with screen tones, dynamic perspective',
|
||||
cartoon: 'soft pastel cartoon, rounded shapes, Saturday-morning animation',
|
||||
'graphic-novel': 'graphic novel, painterly watercolor, muted atmospheric palette',
|
||||
webtoon: 'webtoon, vertical framing, bright saturated colors, soft cel-shading',
|
||||
};
|
||||
|
||||
const VALID_STYLES = Object.keys(STYLE_HINTS) as readonly ComicStyle[];
|
||||
const MAX_SOURCE_TEXT_CHARS = 8_000;
|
||||
const MIN_PANEL_COUNT = 2;
|
||||
const MAX_PANEL_COUNT = 8;
|
||||
|
||||
interface StoryboardRequest {
|
||||
style: ComicStyle;
|
||||
sourceText: string;
|
||||
/** Optional — if omitted we ask for 4 panels (plan default). */
|
||||
panelCount?: number;
|
||||
/** Optional story-level briefing the author wrote at create-time.
|
||||
* Gets prepended to the source-text so Claude knows the tonal
|
||||
* register ("make it funny" / "stay serious"). */
|
||||
storyContext?: string | null;
|
||||
/** Where this text came from — logged only, not sent to the LLM.
|
||||
* Useful for observability ("which module drives most storyboards"). */
|
||||
sourceModule?: string;
|
||||
}
|
||||
|
||||
interface StoryboardPanel {
|
||||
prompt: string;
|
||||
caption?: string;
|
||||
dialogue?: string;
|
||||
}
|
||||
|
||||
interface StoryboardResponse {
|
||||
panels: StoryboardPanel[];
|
||||
model: string;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
function isValidStyle(v: unknown): v is ComicStyle {
|
||||
return typeof v === 'string' && (VALID_STYLES as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
function buildSystemPrompt(style: ComicStyle): string {
|
||||
const hint = STYLE_HINTS[style];
|
||||
return [
|
||||
`You are a comic-story editor. Given a short piece of text (journal entry, note, review, or event description), break it into a sequence of visual comic panels.`,
|
||||
`Style: ${hint}.`,
|
||||
`Return ONLY a JSON object with this exact shape:`,
|
||||
`{"panels": [{"prompt": string, "caption"?: string, "dialogue"?: string}, ...]}`,
|
||||
`Rules:`,
|
||||
`- "prompt" is the visual scene description (what the artist draws). One or two short English sentences. Focus on composition, action, mood, setting. Do NOT describe style — the style prefix is added downstream.`,
|
||||
`- "caption" (optional) is a short narration line rendered at the top or bottom of the panel, max 80 chars. Use sparingly — only when scene-setting or transitions need it.`,
|
||||
`- "dialogue" (optional) is what the protagonist says inside a speech bubble, max 80 chars. Use when the scene has a spoken moment.`,
|
||||
`- Do not number panels. Do not add meta commentary. Do not explain your choices.`,
|
||||
`- The protagonist of every panel is the same person (the story's author).`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildUserPrompt(
|
||||
sourceText: string,
|
||||
panelCount: number,
|
||||
storyContext: string | null | undefined
|
||||
): string {
|
||||
const trimmed = sourceText.trim().slice(0, MAX_SOURCE_TEXT_CHARS);
|
||||
const contextBlock = storyContext?.trim()
|
||||
? `Story briefing from the author:\n${storyContext.trim()}\n\n---\n\n`
|
||||
: '';
|
||||
return [
|
||||
contextBlock,
|
||||
`Source text:\n${trimmed}\n\n---\n\n`,
|
||||
`Generate exactly ${panelCount} panels that tell this as a comic. Output the JSON object described in the system message.`,
|
||||
].join('');
|
||||
}
|
||||
|
||||
const routes = new Hono<{ Variables: AuthVariables }>();
|
||||
|
||||
routes.post('/storyboard', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const body = (await c.req.json()) as Partial<StoryboardRequest>;
|
||||
|
||||
if (!isValidStyle(body.style)) {
|
||||
return c.json({ error: `Invalid style, expected one of: ${VALID_STYLES.join(', ')}` }, 400);
|
||||
}
|
||||
if (!body.sourceText || typeof body.sourceText !== 'string') {
|
||||
return c.json({ error: 'sourceText required' }, 400);
|
||||
}
|
||||
if (body.sourceText.trim().length === 0) {
|
||||
return c.json({ error: 'sourceText must not be blank' }, 400);
|
||||
}
|
||||
|
||||
const panelCount = Math.max(
|
||||
MIN_PANEL_COUNT,
|
||||
Math.min(MAX_PANEL_COUNT, Number(body.panelCount) || 4)
|
||||
);
|
||||
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const parsed = await llmJson<{ panels?: unknown }>({
|
||||
model: STORYBOARD_MODEL,
|
||||
system: buildSystemPrompt(body.style),
|
||||
user: buildUserPrompt(body.sourceText, panelCount, body.storyContext),
|
||||
temperature: 0.7,
|
||||
maxTokens: 2000,
|
||||
});
|
||||
|
||||
const rawPanels = Array.isArray(parsed?.panels) ? parsed.panels : [];
|
||||
// Defense-in-depth: coerce + strip unknown shapes, clamp to
|
||||
// requested count. If the model returns more panels than asked
|
||||
// for we keep the first N; less is fine (fewer credits later).
|
||||
const panels: StoryboardPanel[] = rawPanels
|
||||
.map((raw): StoryboardPanel | null => {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const entry = raw as Record<string, unknown>;
|
||||
const prompt = typeof entry.prompt === 'string' ? entry.prompt.trim() : '';
|
||||
if (!prompt) return null;
|
||||
const caption =
|
||||
typeof entry.caption === 'string' && entry.caption.trim().length > 0
|
||||
? entry.caption.trim().slice(0, 200)
|
||||
: undefined;
|
||||
const dialogue =
|
||||
typeof entry.dialogue === 'string' && entry.dialogue.trim().length > 0
|
||||
? entry.dialogue.trim().slice(0, 200)
|
||||
: undefined;
|
||||
return { prompt: prompt.slice(0, 800), caption, dialogue };
|
||||
})
|
||||
.filter((p): p is StoryboardPanel => p !== null)
|
||||
.slice(0, panelCount);
|
||||
|
||||
const durationMs = Date.now() - startedAt;
|
||||
|
||||
if (panels.length === 0) {
|
||||
logger.warn('comic.storyboard_empty', {
|
||||
userId,
|
||||
style: body.style,
|
||||
sourceModule: body.sourceModule,
|
||||
model: STORYBOARD_MODEL,
|
||||
durationMs,
|
||||
});
|
||||
return c.json(
|
||||
{
|
||||
error: 'Model returned no usable panels',
|
||||
detail: 'Try again, shorten the input, or pick a different style',
|
||||
durationMs,
|
||||
},
|
||||
502
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('comic.storyboard_ok', {
|
||||
userId,
|
||||
style: body.style,
|
||||
sourceModule: body.sourceModule,
|
||||
panelCount: panels.length,
|
||||
model: STORYBOARD_MODEL,
|
||||
durationMs,
|
||||
});
|
||||
|
||||
const response: StoryboardResponse = {
|
||||
panels,
|
||||
model: STORYBOARD_MODEL,
|
||||
durationMs,
|
||||
};
|
||||
return c.json(response);
|
||||
} catch (err) {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
logger.error('comic.storyboard_failed', {
|
||||
userId,
|
||||
style: body.style,
|
||||
sourceModule: body.sourceModule,
|
||||
model: STORYBOARD_MODEL,
|
||||
error: message,
|
||||
status: err instanceof LlmError ? err.status : undefined,
|
||||
durationMs,
|
||||
});
|
||||
return c.json({ error: 'Storyboard generation failed', detail: message, durationMs }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export { routes as comicRoutes };
|
||||
|
|
@ -315,14 +315,12 @@ routes.post('/generate-with-reference', async (c) => {
|
|||
}
|
||||
|
||||
// Ownership check before we spend credits or burn OpenAI quota.
|
||||
// References span two upload tags today:
|
||||
// - `me` — face/body portraits from the profile module
|
||||
// - `comic` — comic-specific anchor / backdrop uploads
|
||||
// Anything outside these apps is treated as not-owned regardless of
|
||||
// mana-media's own view.
|
||||
// Currently only `me` (face/body portraits from the profile module)
|
||||
// is a valid upload tag — anything else is treated as not-owned
|
||||
// regardless of mana-media's own view.
|
||||
try {
|
||||
const { verifyMediaOwnership } = await import('../../lib/media');
|
||||
await verifyMediaOwnership(userId, refIds, ['me', 'comic']);
|
||||
await verifyMediaOwnership(userId, refIds, ['me']);
|
||||
} catch (err) {
|
||||
const e = err as Error & { status?: number; missing?: string[] };
|
||||
if (e.status === 404) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue