From dea8026c5e2aafd5bfb7717ca975379789785c05 Mon Sep 17 00:00:00 2001 From: Bruno Perel Date: Mon, 16 Dec 2024 23:29:52 +0100 Subject: [PATCH] dumili: WIP --- apps/dumili/api/prisma/schema.prisma | 61 +++++++++---- apps/dumili/api/services/indexation/index.ts | 89 +++++++++++-------- apps/dumili/api/services/indexation/types.ts | 31 +++++-- .../src/components/AiSuggestionIcon.vue | 9 +- apps/dumili/src/components/AiTooltip.vue | 28 +++--- apps/dumili/src/components/Entry.vue | 36 +++----- apps/dumili/src/components/Gallery.vue | 8 +- .../src/components/IssueSuggestionList.vue | 4 +- .../src/components/IssueSuggestionModal.vue | 6 +- apps/dumili/src/components/StoryKindBadge.vue | 15 ++++ .../src/components/StorySuggestionList.vue | 2 +- .../dumili/src/components/TableOfContents.vue | 2 +- .../src/components/TableOfContentsEntry.vue | 19 ++-- .../src/components/TableOfContentsPage.vue | 11 +-- apps/dumili/src/composables/useAi.ts | 12 +++ apps/dumili/src/composables/useHint.ts | 2 +- apps/dumili/src/stores/suggestions.ts | 4 +- apps/dumili/src/style.scss | 18 +++- 18 files changed, 211 insertions(+), 146 deletions(-) create mode 100644 apps/dumili/src/components/StoryKindBadge.vue diff --git a/apps/dumili/api/prisma/schema.prisma b/apps/dumili/api/prisma/schema.prisma index 847b29255..c49f68101 100644 --- a/apps/dumili/api/prisma/schema.prisma +++ b/apps/dumili/api/prisma/schema.prisma @@ -33,13 +33,13 @@ model entry { model image { id Int @id @default(autoincrement()) - url String @unique(map: "page_image_url_uindex") @db.VarChar(255) + url String @unique(map: "image_url_uindex") @db.VarChar(255) aiKumikoInferredStoryKind pageImageAiKumikoInferredStoryKind? @map("ai_kumiko_inferred_story_kind") aiKumikoResultPanels aiKumikoResultPanel[] aiOcrResults aiOcrResult[] page page[] - @@map("page_image") + @@map("image") } model page { @@ -47,7 +47,7 @@ model page { pageNumber Int @map("page_number") @db.SmallInt indexationId String @map("indexation_id") @db.VarChar(20) imageId Int? @map("image_id") - image image? @relation(fields: [imageId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "page_image_id_fk") + image image? @relation(fields: [imageId], references: [id], onDelete: Restrict, onUpdate: Restrict, map: "page_image_id_fk") indexation indexation @relation(fields: [indexationId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "page_indexation_id_fk") @@unique([indexationId, pageNumber], map: "page_indexation_id_page_number_uindex") @@ -70,15 +70,14 @@ model indexation { } model storyKindSuggestion { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) kind storyKind - isChosenByAi Boolean @default(false) @map("is_chosen_by_ai") - entryId Int @map("entry_id") - acceptedOnEntries entry[] @relation("entry_accepted_story_kind_suggested_idTostory_kind_suggestion") - entry entry @relation("story_kind_suggestion_entry_idToentry", fields: [entryId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "story_kind_suggestion_entry_id_fk") + entryId Int @map("entry_id") + acceptedOnEntries entry[] @relation("entry_accepted_story_kind_suggested_idTostory_kind_suggestion") + entry entry @relation("story_kind_suggestion_entry_idToentry", fields: [entryId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "story_kind_suggestion_entry_id_fk") + ai storyKindSuggestionAi? @@index([entryId], map: "story_kind_suggestion_entry_id_fk") - @@index([isChosenByAi], map: "story_kind_suggestion_ai_source_page_id_fk") @@map("story_kind_suggestion") } @@ -87,10 +86,10 @@ model storySuggestion { storycode String @db.VarChar(19) entryId Int @map("entry_id") ocrDetailsId Int? @map("ocr_details_id") - isChosenByAi Boolean @map("is_chosen_by_ai") ocrDetails aiOcrPossibleStory[] acceptedOnEntries entry[] @relation("entry_accepted_story_suggested_idTostory_suggestion") entry entry @relation(fields: [entryId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "story_suggestion_entry_id_fk") + ai storySuggestionAi? @@index([entryId], map: "story_suggestion_entry_id_fk") @@index([ocrDetailsId], map: "story_suggestion_ai_ocr_possible_story_id_fk") @@ -98,14 +97,14 @@ model storySuggestion { } model issueSuggestion { - id Int @id @default(autoincrement()) - indexationId String @map("indexation_id") @db.VarChar(20) - isChosenByAi Boolean @map("is_chosen_by_ai") - publicationcode String @db.VarChar(12) - issuenumber String @db.VarChar(13) - issuecode String @db.VarChar(25) - acceptedOnEntries indexation[] @relation("indexation_accepted_issue_suggestion_idToissue_suggestion") - indexation indexation @relation(fields: [indexationId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "issue_suggestion_indexation_id_fk") + id Int @id @default(autoincrement()) + indexationId String @map("indexation_id") @db.VarChar(20) + publicationcode String @db.VarChar(12) + issuenumber String @db.VarChar(13) + issuecode String @db.VarChar(25) + acceptedOnEntries indexation[] @relation("indexation_accepted_issue_suggestion_idToissue_suggestion") + indexation indexation @relation(fields: [indexationId], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "issue_suggestion_indexation_id_fk") + ai issueSuggestionAi? @@index([indexationId], map: "issue_suggestion_indexation_id_fk") @@map("issue_suggestion") @@ -134,7 +133,7 @@ model aiOcrResult { y4 Int text String @db.Text confidence Float @db.Float - image image @relation(fields: [imageId], references: [id], onUpdate: Restrict, map: "ai_ocr_result_page_image_id_fk") + image image @relation(fields: [imageId], references: [id], onUpdate: Restrict, map: "ai_ocr_result_image_id_fk") @@index([imageId], map: "ai_ocr_result_image_id_index") @@map("ai_ocr_result") @@ -153,6 +152,30 @@ model aiKumikoResultPanel { @@map("ai_kumiko_result_panel") } +model issueSuggestionAi { + id Int @id @default(autoincrement()) + suggestionId Int @unique(map: "issue_suggestion_ai_issue_suggestion_id_unique_fk") @map("suggestion_id") + issueSuggestion issueSuggestion @relation(fields: [suggestionId], references: [id], onUpdate: Restrict, map: "issue_suggestion_ai_issue_suggestion_id_unique_fk") + + @@map("issue_suggestion_ai") +} + +model storyKindSuggestionAi { + id Int @id @default(autoincrement()) + suggestionId Int @unique(map: "story_kind_suggestion_ai_story_kind_suggestion_id_unique_fk") @map("suggestion_id") + storyKindSuggestion storyKindSuggestion @relation(fields: [suggestionId], references: [id], onUpdate: Restrict, map: "story_kind_suggestion_ai_story_kind_suggestion_id_unique_fk") + + @@map("story_kind_suggestion_ai") +} + +model storySuggestionAi { + id Int @id @default(autoincrement()) + suggestionId Int @unique(map: "story_suggestion_ai_story_suggestion_id_unique_fk") @map("suggestion_id") + storySuggestion storySuggestion @relation(fields: [suggestionId], references: [id], onUpdate: Restrict, map: "story_suggestion_ai_story_suggestion_id_fk") + + @@map("story_suggestion_ai") +} + enum storyKind { a c diff --git a/apps/dumili/api/services/indexation/index.ts b/apps/dumili/api/services/indexation/index.ts index bd24eff31..4d61549ad 100644 --- a/apps/dumili/api/services/indexation/index.ts +++ b/apps/dumili/api/services/indexation/index.ts @@ -233,9 +233,11 @@ export default (io: Server) => { }, data: { storySuggestions: { - create: storyResults.map(({ storycode, score }) => ({ + create: storyResults.map(({ storycode, score }): Prisma.storySuggestionCreateWithoutEntryInput => ({ storycode, - isChosenByAi: true, + ai: { + create: {}, + }, ocrDetails: { create: { score, @@ -269,7 +271,7 @@ export default (io: Server) => { const setInferredEntryStoryKind = async (entryId: entry["id"]) => { indexationSocket.emit("setInferredEntryStoryKind", entryId); - const indexation = indexationSocket.data.indexation; + const {indexation} = indexationSocket.data; const mostInferredStoryKind = ( await prisma.image.groupBy({ by: ["aiKumikoInferredStoryKind"], @@ -286,41 +288,36 @@ export default (io: Server) => { }, }, }) - ).pop()!.aiKumikoInferredStoryKind!; + ).pop(); const entryIdx = indexationSocket.data.indexation.entries.findIndex( ({ id }) => id === entryId ); console.log( - `Kumiko: entry #${entryIdx}: inferred story kind is ${mostInferredStoryKind}` + `Kumiko: entry #${entryIdx}: inferred story kind is ${mostInferredStoryKind?.aiKumikoInferredStoryKind}` ); - const newEntry = await prisma.entry.update({ - include: { - storyKindSuggestions: true, - }, + await prisma.storyKindSuggestionAi.deleteMany({ where: { - id: entryId, - }, - data: { - storyKindSuggestions: { - deleteMany: { - isChosenByAi: true, - }, - updateMany: { - data: { - isChosenByAi: true, - }, - where: { - kind: mostInferredStoryKind, - }, - }, + suggestionId: { + in: indexation.entries[entryIdx].storyKindSuggestions.map( + ({ id }) => id + ) }, - }, - }); + }}); + + let createdSuggestionAi = null + if (mostInferredStoryKind?.aiKumikoInferredStoryKind) { + createdSuggestionAi = await prisma.storyKindSuggestionAi.create({ + data: { + suggestionId: indexation.entries[entryIdx].storyKindSuggestions.find(({ kind }) => kind === mostInferredStoryKind.aiKumikoInferredStoryKind)!.id + } + }); + } + indexationSocket.emit("setInferredEntryStoryKindEnd", entryId); - return newEntry; + return createdSuggestionAi }; indexationSocket.on("setPageUrl", async (id, url, callback) => { @@ -679,10 +676,11 @@ export default (io: Server) => { return; } - const entry = getEntryFromPage(indexation, pageId)!; - await setInferredEntryStoryKind(entry.id); - - callback({ status: "OK" }); + setKumikoInferredPageStoryKinds(page, true).finally(async () => { + const entry = getEntryFromPage(indexation, pageId)!; + await setInferredEntryStoryKind(entry.id); + callback({ status: "OK" }); + }) }); indexationSocket.on( @@ -707,6 +705,18 @@ export default (io: Server) => { } ); + indexationSocket.on("inferEntryStoryKind", async (entryId, callback) => { + if (indexationSocket.data.indexation.entries.some(({ id }) => id === entryId)) { + return setInferredEntryStoryKind(entryId).then(() => callback({ status: "OK" })); + } + else { + callback({ + error: `This indexation does not have any entry with this ID`, + errorDetails: JSON.stringify({ entryId }), + }); + } + }); + indexationSocket.on("createEntry", async (callback) => createEntry(indexationSocket.data.indexation.id) .then(async ({ id }) => { @@ -716,13 +726,14 @@ export default (io: Server) => { return setKumikoInferredPageStoryKinds( getEntryPages(indexationSocket.data.indexation, id)[0] ).finally(() => - setInferredEntryStoryKind(id).then((entry) => - acceptStoryKindSuggestion( - entry.storyKindSuggestions.find( - ({ isChosenByAi }) => isChosenByAi - )!.id, - entry.id - ) + setInferredEntryStoryKind(id).then((inferredSuggestion) => + { + if (inferredSuggestion) { + return acceptStoryKindSuggestion(inferredSuggestion?.suggestionId, + id + ); + } + } ) ); }) @@ -735,7 +746,7 @@ const inferStoryKindFromAiResults = ( panelsOfPage: KumikoProcessedResult[], pageNumber: number ) => - panelsOfPage.length === 1 ? (pageNumber === 1 ? COVER : ILLUSTRATION) : STORY; + pageNumber === 1 ? COVER : (panelsOfPage.length === 1 ? ILLUSTRATION : STORY); export const acceptStoryKindSuggestion = ( suggestionId: storyKindSuggestion["id"] | null, diff --git a/apps/dumili/api/services/indexation/types.ts b/apps/dumili/api/services/indexation/types.ts index c2e771ebd..f526d6674 100644 --- a/apps/dumili/api/services/indexation/types.ts +++ b/apps/dumili/api/services/indexation/types.ts @@ -20,8 +20,16 @@ export const indexationPayloadInclude = { }, }, }, - acceptedIssueSuggestion: true, - issueSuggestions: true, + acceptedIssueSuggestion: { + include: { + ai: true, + }, + }, + issueSuggestions: { + include: { + ai: true, + }, + }, entries: { include: { acceptedStory: { @@ -29,10 +37,11 @@ export const indexationPayloadInclude = { ocrDetails: true, }, }, - acceptedStoryKind: true, - storyKindSuggestions: true, + acceptedStoryKind: {include: {ai: true}}, + storyKindSuggestions: {include: {ai: true}}, storySuggestions: { include: { + ai: true, ocrDetails: true, }, }, @@ -69,7 +78,7 @@ export default abstract class { ) => void; abstract createStorySuggestion: ( - suggestion: Prisma.storySuggestionUncheckedCreateInput, + suggestion: Prisma.storySuggestionUncheckedCreateInput & {ai: boolean}, callback: ( data: Errorable< { createdStorySuggestion: Pick }, @@ -93,7 +102,7 @@ export default abstract class { suggestion: Omit< Prisma.issueSuggestionUncheckedCreateInput, "indexationId" - >, + > & {ai: boolean}, callback: (data: { suggestionId: storySuggestion["id"] }) => void, ) => void; @@ -138,6 +147,16 @@ export default abstract class { ) => void, ) => void; + abstract inferEntryStoryKind: ( + entryId: entry["id"], + callback: ( + data: Errorable< + { status: "OK" }, + | "This indexation does not have any entry with this ID" + >, + ) => void, + ) => void; + abstract runKumikoOnPage: ( pageId: page["id"], callback: ( diff --git a/apps/dumili/src/components/AiSuggestionIcon.vue b/apps/dumili/src/components/AiSuggestionIcon.vue index 9e0e1e778..b09dedf81 100644 --- a/apps/dumili/src/components/AiSuggestionIcon.vue +++ b/apps/dumili/src/components/AiSuggestionIcon.vue @@ -41,14 +41,15 @@ const component = computed(() => svg { width: 20px; + height: 20px; min-width: 20px; + padding: 4px; &.button { - cursor: help; - background: black; + background: lightgrey; border-radius: 10px; - padding: 4px; - height: 20px; + opacity: 0.5; + cursor: help; } &.idle { diff --git a/apps/dumili/src/components/AiTooltip.vue b/apps/dumili/src/components/AiTooltip.vue index efdfeb09c..fde722a56 100644 --- a/apps/dumili/src/components/AiTooltip.vue +++ b/apps/dumili/src/components/AiTooltip.vue @@ -1,35 +1,28 @@ \ No newline at end of file diff --git a/apps/dumili/src/components/StorySuggestionList.vue b/apps/dumili/src/components/StorySuggestionList.vue index 2644e0692..0771801ab 100644 --- a/apps/dumili/src/components/StorySuggestionList.vue +++ b/apps/dumili/src/components/StorySuggestionList.vue @@ -81,7 +81,7 @@ const acceptStory = async (storycode: storySuggestion["storycode"] | null) => { { entryId: entry.value.id, storycode, - isChosenByAi: false, + ai: false, }, ); storySuggestion = result.createdStorySuggestion; diff --git a/apps/dumili/src/components/TableOfContents.vue b/apps/dumili/src/components/TableOfContents.vue index 5918adfd2..25173c76c 100644 --- a/apps/dumili/src/components/TableOfContents.vue +++ b/apps/dumili/src/components/TableOfContents.vue @@ -125,7 +125,7 @@ const numberOfPages = computed({ }); const issueAiSuggestion = computed(() => - indexation.value.issueSuggestions.find(({ isChosenByAi }) => isChosenByAi), + indexation.value.issueSuggestions.find(({ ai }) => !!ai), ); const updateNumberOfPages = (event: Event) => { diff --git a/apps/dumili/src/components/TableOfContentsEntry.vue b/apps/dumili/src/components/TableOfContentsEntry.vue index c9476b18e..788e0e784 100644 --- a/apps/dumili/src/components/TableOfContentsEntry.vue +++ b/apps/dumili/src/components/TableOfContentsEntry.vue @@ -28,7 +28,10 @@ }" :show="hoveredEntry?.id === entry.id" :status="storyKindAiSuggestion?.kind ? 'success' : 'idle'" - :on-click-rerun="() => runStorycodeOcr(entry.id)" + :on-click-rerun=" + () => + inferEntryStoryKind(entry.id).then(() => runStorycodeOcr(entry.id)) + " @click="showAiDetectionsOn = { type: 'entry', id: entry.id }" @blur="showAiDetectionsOn = undefined" > @@ -41,12 +44,10 @@ })) " />
- {{ $t("Type d'entrée déduit") }} - {{ - storyKindAiSuggestion - ? storyKinds[storyKindAiSuggestion?.kind] - : $t("Non calculé") - }} +
+ {{ $t("Type d'entrée déduit") }} +
+