bilder gen
This commit is contained in:
1 parent
9ecb292c26
commit
f4aac46ea0
9 files changed
+1381
-93
No files matched your search
+254
-32
@@ -1,5 +1,5 @@
|
||||
import { ID, Query, databaseId, tables } from './appwrite';
|
||||
import { BUCKET_REFERENZEN as BUCKET } from './dateien';
|
||||
import { BUCKET_BILDER, BUCKET_REFERENZEN as BUCKET } from './dateien';
|
||||
import { teamRechte } from './permissions';
|
||||
import { hochladen, type Auswahl } from './upload';
|
||||
|
||||
@@ -26,7 +26,11 @@ export type AssetVersion = {
|
||||
status: 'entwurf' | 'freigegeben' | 'archiviert';
|
||||
beschreibung_md?: string;
|
||||
merkmale?: string[];
|
||||
change_reason?: string;
|
||||
/** EINGABE – was der Nutzer hochgeladen hat. */
|
||||
reference_file_ids?: string[];
|
||||
/** ERGEBNIS – das für diese Version erzeugte Bild. */
|
||||
bild_file_id?: string;
|
||||
};
|
||||
|
||||
export const TYPEN: { wert: AssetTyp; titel: string; erklaerung: string; icon: string }[] = [
|
||||
@@ -64,16 +68,54 @@ export async function versionenZuAsset(assetId: string): Promise<AssetVersion[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt ein Modell samt erster Version an und gibt sie sofort frei.
|
||||
* Wieviele Varianten beim Anlegen erzeugt werden.
|
||||
*
|
||||
* Release-Prinzip (projekt-uebersicht.md §4): Bilder verwenden immer eine
|
||||
* *freigegebene* Version, damit nichts unbemerkt wegdriftet. Version 1 wird
|
||||
* hier direkt freigegeben, weil es sonst nichts gäbe, womit man arbeiten kann.
|
||||
* Vier, weil §8 vier bis sechs Referenzbilder als Optimum nennt und weil man
|
||||
* aus vier Vorschlägen noch bewusst auswählt – bei acht klickt man das erste
|
||||
* Passable an, und das Auswahlsignal wird wertlos.
|
||||
*/
|
||||
export const VARIANTEN_JE_MODELL = 4;
|
||||
|
||||
/**
|
||||
* Typen, für die beim Anlegen Varianten erzeugt werden.
|
||||
*
|
||||
* **Logo ist ein Sonderfall.** Die Brand-Regel verbietet, das Markenzeichen zu
|
||||
* verzerren, zu beschneiden, umzufärben oder neu zu interpretieren. Sie schützt
|
||||
* damit ein **vorhandenes** Logo – eine Marke ohne Logo hat nichts zu schützen.
|
||||
*
|
||||
* Daraus folgen zwei Wege, die sich gegenseitig ausschließen:
|
||||
* - **Logo hochladen** → wird v1 und sofort freigegeben. Ab da wird es nie
|
||||
* wieder generiert, nur noch unverändert eingesetzt. Das ist der Normalfall.
|
||||
* - **Logo entwerfen lassen** → nur wenn *keine* Datei hochgeladen wird. Es gibt
|
||||
* dann kein Original, das verfremdet werden könnte; es entsteht ein Vorschlag.
|
||||
*/
|
||||
export const GENERIERBAR: AssetTyp[] = ['gesicht', 'produkt', 'kulisse'];
|
||||
|
||||
/** Kann für diesen Typ und diese Eingabe generiert werden? */
|
||||
export function kannGenerieren(typ: AssetTyp, hatReferenzbilder: boolean): boolean {
|
||||
if (GENERIERBAR.includes(typ)) return true;
|
||||
// Logo: nur entwerfen, wenn nichts hochgeladen wurde.
|
||||
return typ === 'logo' && !hatReferenzbilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt ein Modell an.
|
||||
*
|
||||
* **Logo:** nur hochladen. Eine der globalen Brand-Regeln verbietet es
|
||||
* ausdrücklich, das Logo generieren zu lassen – es darf nie verfremdet,
|
||||
* beschnitten oder neu interpretiert werden. Version 1 ist deshalb direkt die
|
||||
* freigegebene Version mit der hochgeladenen Datei.
|
||||
*
|
||||
* **Person, Produkt, Kulisse:** vier Entwurfs-Versionen plus je ein
|
||||
* `modell_gen`-Job. Freigegeben wird **nichts** – welche Variante gewinnt,
|
||||
* entscheidet der Nutzer im Ergebnis-Screen. Das ist das Release-Prinzip aus
|
||||
* §4: es wird immer eine bewusst freigegebene Version verwendet, nie die
|
||||
* neueste, damit das Aussehen nicht unbemerkt wegdriftet.
|
||||
*/
|
||||
export async function assetAnlegen(
|
||||
brandId: string,
|
||||
teamId: string,
|
||||
daten: { typ: AssetTyp; name: string; beschreibung: string; bilder: Auswahl[] },
|
||||
daten: { typ: AssetTyp; name: string; beschreibung: string; merkmale: string[]; bilder: Auswahl[] },
|
||||
): Promise<Asset> {
|
||||
const rechte = teamRechte(teamId);
|
||||
|
||||
@@ -90,35 +132,205 @@ export async function assetAnlegen(
|
||||
permissions: rechte,
|
||||
})) as unknown as Asset;
|
||||
|
||||
const version = (await tables.createRow({
|
||||
const basis = {
|
||||
asset_id: asset.$id,
|
||||
beschreibung_md: daten.beschreibung,
|
||||
// Token-Locking: diese Zeichenfolgen setzt der Prompt **wörtlich** ein.
|
||||
// Umschreiben heißt hier, die Identität zu verlieren.
|
||||
merkmale: daten.merkmale,
|
||||
reference_file_ids: fileIds,
|
||||
};
|
||||
|
||||
if (!kannGenerieren(daten.typ, fileIds.length > 0)) {
|
||||
const version = (await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'asset_versions',
|
||||
rowId: ID.unique(),
|
||||
data: { ...basis, version_no: 1, status: 'freigegeben' },
|
||||
permissions: rechte,
|
||||
})) as unknown as AssetVersion;
|
||||
|
||||
await tables.updateRow({
|
||||
databaseId, tableId: 'assets', rowId: asset.$id,
|
||||
data: { released_version_id: version.$id },
|
||||
});
|
||||
return { ...asset, released_version_id: version.$id };
|
||||
}
|
||||
|
||||
for (let i = 1; i <= VARIANTEN_JE_MODELL; i++) {
|
||||
const version = (await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'asset_versions',
|
||||
rowId: ID.unique(),
|
||||
data: { ...basis, version_no: i, status: 'entwurf', change_reason: `Variante ${i} der ersten Runde` },
|
||||
permissions: rechte,
|
||||
})) as unknown as AssetVersion;
|
||||
|
||||
await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'jobs',
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
brand_id: brandId,
|
||||
typ: 'modell_gen',
|
||||
prompt_template_key: 'P7',
|
||||
status: 'wartend',
|
||||
refs: JSON.stringify({ asset_id: asset.$id, version_id: version.$id, variante: i }),
|
||||
},
|
||||
permissions: rechte,
|
||||
});
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet eine neue Runde Varianten auf einem bestehenden Modell.
|
||||
*
|
||||
* Die freigegebene Version bleibt freigegeben, bis der Nutzer bewusst eine neue
|
||||
* wählt – das Release-Prinzip aus §4 gilt auch hier. Bis dahin arbeitet die App
|
||||
* unverändert weiter mit dem alten Stand, statt heimlich auf einen Entwurf
|
||||
* umzuschwenken.
|
||||
*
|
||||
* `grund` beschreibt, was anders werden soll. Er landet in `change_reason` und
|
||||
* geht zusätzlich in den Prompt – ohne ihn wäre eine zweite Runde nur ein
|
||||
* neuer Würfelwurf mit demselben Text.
|
||||
*/
|
||||
export async function neueRunde(
|
||||
brandId: string,
|
||||
teamId: string,
|
||||
assetId: string,
|
||||
daten: { beschreibung: string; merkmale: string[]; grund: string },
|
||||
): Promise<void> {
|
||||
const rechte = teamRechte(teamId);
|
||||
const vorhanden = await versionenZuAsset(assetId);
|
||||
const start = Math.max(0, ...vorhanden.map((v) => v.version_no));
|
||||
// Referenzen der ersten Version mitnehmen – sie sind die Vorlage des Modells.
|
||||
const referenzen = vorhanden.sort((a, b) => a.version_no - b.version_no)[0]?.reference_file_ids ?? [];
|
||||
|
||||
for (let i = 1; i <= VARIANTEN_JE_MODELL; i++) {
|
||||
const version = (await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'asset_versions',
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
asset_id: assetId,
|
||||
version_no: start + i,
|
||||
status: 'entwurf',
|
||||
beschreibung_md: daten.beschreibung,
|
||||
merkmale: daten.merkmale,
|
||||
reference_file_ids: referenzen,
|
||||
change_reason: daten.grund,
|
||||
},
|
||||
permissions: rechte,
|
||||
})) as unknown as AssetVersion;
|
||||
|
||||
await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'jobs',
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
brand_id: brandId,
|
||||
typ: 'modell_gen',
|
||||
prompt_template_key: 'P7',
|
||||
status: 'wartend',
|
||||
refs: JSON.stringify({ asset_id: assetId, version_id: version.$id, variante: i }),
|
||||
},
|
||||
permissions: rechte,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type VariantenLage = { status: string; error?: string; variante: number };
|
||||
|
||||
/**
|
||||
* Der Job-Zustand je Version.
|
||||
*
|
||||
* Nötig, weil man einer Version selbst nicht ansieht, ob ihr Bild noch aussteht
|
||||
* oder die Generierung gescheitert ist – beides sieht nach „Entwurf ohne Bild"
|
||||
* aus. Der Grund steht ausschließlich im Job.
|
||||
*/
|
||||
export async function jobsZuAsset(assetId: string): Promise<Map<string, VariantenLage>> {
|
||||
const res = await tables.listRows({
|
||||
databaseId,
|
||||
tableId: 'asset_versions',
|
||||
tableId: 'jobs',
|
||||
queries: [Query.equal('typ', 'modell_gen'), Query.orderDesc('$createdAt'), Query.limit(100)],
|
||||
});
|
||||
const nach = new Map<string, VariantenLage>();
|
||||
for (const j of res.rows as unknown as { status: string; error?: string; refs?: string }[]) {
|
||||
try {
|
||||
const r = JSON.parse(j.refs ?? '{}');
|
||||
if (r.asset_id !== assetId || !r.version_id) continue;
|
||||
if (!nach.has(r.version_id)) nach.set(r.version_id, { status: j.status, error: j.error, variante: r.variante ?? 1 });
|
||||
} catch { /* kaputte refs überspringen */ }
|
||||
}
|
||||
return nach;
|
||||
}
|
||||
|
||||
/** Stellt eine gescheiterte Variante erneut in die Warteschlange. */
|
||||
export async function varianteWiederholen(
|
||||
brandId: string,
|
||||
teamId: string,
|
||||
assetId: string,
|
||||
versionId: string,
|
||||
variante: number,
|
||||
): Promise<void> {
|
||||
await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'jobs',
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
asset_id: asset.$id,
|
||||
version_no: 1,
|
||||
status: 'freigegeben',
|
||||
beschreibung_md: daten.beschreibung,
|
||||
reference_file_ids: fileIds,
|
||||
brand_id: brandId,
|
||||
typ: 'modell_gen',
|
||||
prompt_template_key: 'P7',
|
||||
status: 'wartend',
|
||||
refs: JSON.stringify({ asset_id: assetId, version_id: versionId, variante }),
|
||||
},
|
||||
permissions: rechte,
|
||||
})) as unknown as AssetVersion;
|
||||
permissions: teamRechte(teamId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine Version frei und archiviert die konkurrierenden.
|
||||
*
|
||||
* Archivieren statt löschen – ein verworfener Entwurf ist auch Wissen (§5), und
|
||||
* `won_against_version_id` hält fest, wogegen sich die Siegerin durchgesetzt hat.
|
||||
*/
|
||||
export async function versionFreigeben(assetId: string, versionId: string): Promise<void> {
|
||||
const alle = await versionenZuAsset(assetId);
|
||||
// Auch die **abgelöste** Version wird archiviert, nicht nur die Entwürfe.
|
||||
// Bliebe sie auf `freigegeben` stehen, gäbe es zwei Zeilen mit diesem Status,
|
||||
// von denen nur eine wirklich gilt – und die Anzeige zeigte sie als Entwurf.
|
||||
const verlierer = alle.filter((v) => v.$id !== versionId && v.status !== 'archiviert');
|
||||
|
||||
await tables.updateRow({
|
||||
databaseId,
|
||||
tableId: 'assets',
|
||||
rowId: asset.$id,
|
||||
data: { released_version_id: version.$id },
|
||||
databaseId, tableId: 'asset_versions', rowId: versionId,
|
||||
data: {
|
||||
status: 'freigegeben',
|
||||
won_against_version_id: verlierer[0]?.$id ?? null,
|
||||
},
|
||||
});
|
||||
for (const v of verlierer) {
|
||||
await tables.updateRow({
|
||||
databaseId, tableId: 'asset_versions', rowId: v.$id, data: { status: 'archiviert' },
|
||||
});
|
||||
}
|
||||
await tables.updateRow({
|
||||
databaseId, tableId: 'assets', rowId: assetId, data: { released_version_id: versionId },
|
||||
});
|
||||
|
||||
return { ...asset, released_version_id: version.$id };
|
||||
}
|
||||
|
||||
export async function assetLoeschen(id: string): Promise<void> {
|
||||
await tables.deleteRow({ databaseId, tableId: 'assets', rowId: id });
|
||||
}
|
||||
|
||||
export type AssetMitBild = Asset & { titelbildId?: string };
|
||||
export type AssetMitBild = Asset & {
|
||||
titelbildId?: string;
|
||||
/** In welchem Bucket das Titelbild liegt – generiert oder hochgeladen. */
|
||||
titelbildBucket?: string;
|
||||
/** true, solange keine Version freigegeben ist (Generierung läuft noch). */
|
||||
wartet?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modelle samt Titelbild – das erste Referenzbild der **freigegebenen** Version.
|
||||
@@ -129,21 +341,31 @@ export type AssetMitBild = Asset & { titelbildId?: string };
|
||||
*/
|
||||
export async function assetsMitBild(brandId: string, typ?: AssetTyp): Promise<AssetMitBild[]> {
|
||||
const liste = await assetListe(brandId, typ);
|
||||
const versionIds = liste.map((a) => a.released_version_id).filter((v): v is string => !!v);
|
||||
if (!versionIds.length) return liste;
|
||||
if (!liste.length) return liste;
|
||||
|
||||
// Alle Versionen aller Modelle in einer Abfrage – nicht eine je Modell.
|
||||
const res = await tables.listRows({
|
||||
databaseId,
|
||||
tableId: 'asset_versions',
|
||||
queries: [Query.equal('$id', versionIds), Query.limit(100)],
|
||||
queries: [Query.equal('asset_id', liste.map((a) => a.$id)), Query.limit(200)],
|
||||
});
|
||||
const versionen = res.rows as unknown as AssetVersion[];
|
||||
|
||||
return liste.map((a) => {
|
||||
const meine = versionen.filter((v) => v.asset_id === a.$id);
|
||||
const frei = meine.find((v) => v.$id === a.released_version_id);
|
||||
// Ist noch nichts freigegeben, zeigt die Kachel den ersten fertigen Entwurf –
|
||||
// sonst bliebe sie leer, obwohl schon Varianten da sind.
|
||||
const ersatz = meine.find((v) => v.bild_file_id) ?? meine[0];
|
||||
const quelle = frei ?? ersatz;
|
||||
|
||||
return {
|
||||
...a,
|
||||
titelbildId: quelle?.bild_file_id ?? quelle?.reference_file_ids?.[0],
|
||||
titelbildBucket: quelle?.bild_file_id ? BUCKET_BILDER : BUCKET,
|
||||
wartet: !a.released_version_id,
|
||||
};
|
||||
});
|
||||
const nachId = new Map(
|
||||
(res.rows as unknown as AssetVersion[]).map((v) => [v.$id, v.reference_file_ids?.[0]]),
|
||||
);
|
||||
return liste.map((a) => ({
|
||||
...a,
|
||||
titelbildId: a.released_version_id ? nachId.get(a.released_version_id) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user