diff --git a/apps/api/src/features/guardians/guardian.route.ts b/apps/api/src/features/guardians/guardian.route.ts index 9b42373..c584624 100644 --- a/apps/api/src/features/guardians/guardian.route.ts +++ b/apps/api/src/features/guardians/guardian.route.ts @@ -7,9 +7,6 @@ import { handleError } from '@peace-net/shared/utils/error-handler' import { Hono } from 'hono' import { getEnv } from '~/config/environment' -import { GuardianController } from '~/features/guardians/guardian.controller' -import { GuardianService } from '~/features/guardians/guardian.service' -import { GuardianUseCase } from '~/features/guardians/guardian.usecase' import { UsageLogRepository } from '~/features/usageLogs/usageLog.repository' import { UsageLogService } from '~/features/usageLogs/usageLog.service' import { UsageFacade } from '~/features/usages/usage.facade' @@ -18,11 +15,18 @@ import { UserPlanService } from '~/features/userPlans/userPlan.service' import { AnthropicClient, GoogleClient, OpenAIClient } from '~/libs/models' import { SupabaseClient } from '~/libs/supabase' +import { GuardianImageController } from './guardianImage.controller' +import { GuardianImageService } from './guardianImage.service' +import { GuardianImageUseCase } from './guardianImage.usecase' +import { GuardianTextController } from './guardianText.controller' +import { GuardianTextService } from './guardianText.service' +import { GuardianTextUseCase } from './guardianText.usecase' + /** * Guardian APIのルーティングを定義します * テキストと画像のコンテンツモデレーション用エンドポイントを提供 * - POST /text: テキスト分析 - * - POST /image: 画像分析(未実装) + * - POST /image: 画像分析 */ const guardianRoutes = new Hono() @@ -35,9 +39,9 @@ guardianRoutes.post( return }), async (c) => { - return new GuardianController( - new GuardianUseCase( - new GuardianService( + return new GuardianTextController( + new GuardianTextUseCase( + new GuardianTextService( OpenAIClient(getEnv(c).OPENAI_API_KEY), AnthropicClient(getEnv(c).ANTHROPIC_API_KEY), GoogleClient(getEnv(c).GOOGLE_API_KEY), @@ -74,13 +78,9 @@ guardianRoutes.post( return }), async (c) => { - return new GuardianController( - new GuardianUseCase( - new GuardianService( - OpenAIClient(getEnv(c).OPENAI_API_KEY), - AnthropicClient(getEnv(c).ANTHROPIC_API_KEY), - GoogleClient(getEnv(c).GOOGLE_API_KEY), - ), + return new GuardianImageController( + new GuardianImageUseCase( + new GuardianImageService(OpenAIClient(getEnv(c).OPENAI_API_KEY)), new UsageFacade( new UserPlanService( new UserPlanRepository( diff --git a/apps/api/src/features/guardians/guardianImage.controller.ts b/apps/api/src/features/guardians/guardianImage.controller.ts new file mode 100644 index 0000000..7b1b924 --- /dev/null +++ b/apps/api/src/features/guardians/guardianImage.controller.ts @@ -0,0 +1,37 @@ +import { GuardianImageDTO } from '@peace-net/shared/types/guardian' +import { Context } from 'hono' + +import { IGuardianImageUseCase } from './guardianImage.usecase' + +/** + * 画像の不適切な内容を分析し、カテゴリー別のスコアを提供するコントローラー + * + * このコントローラーは、GuardianUseCaseを使用してテキストの不適切な内容を分析し、結果をクライアントに返します。 + * + * @class GuardianImageController + * @method guardianImage - 指定された画像を分析し、不適切な内容の有無を判定します + * @param guardianImageUseCase - GuardianUseCaseのインスタンス + * @returns 分析結果を含むJSONレスポンス + * @throws エラーが発生した場合はエラーレスポンスを返します + */ +export class GuardianImageController { + constructor(private guardianImageUseCase: IGuardianImageUseCase) {} + + async guardianImage(c: Context) { + const dto = (await c.req.json()) as GuardianImageDTO + + const userId = c.get('userId') as string + const apiKeyId = c.get('apiKeyId') as string + const result = await this.guardianImageUseCase.guardianImage({ + ...dto, + userId, + apiKeyId, + }) + + if (!result.ok) { + throw result.error + } + + return c.json(result.value) + } +} diff --git a/apps/api/src/features/guardians/guardianImage.service.ts b/apps/api/src/features/guardians/guardianImage.service.ts new file mode 100644 index 0000000..a58bab4 --- /dev/null +++ b/apps/api/src/features/guardians/guardianImage.service.ts @@ -0,0 +1,80 @@ +import { OpenAIProvider } from '@ai-sdk/openai' +import { InternalServerError } from '@peace-net/shared/core/error' +import { categoryScoresSchema } from '@peace-net/shared/schemas/guardian' +import type { CategoryScores } from '@peace-net/shared/types/guardian' +import { generateObject } from 'ai' +import { z } from 'zod' + +const systemPrompt = ` +# 役割 +あなたは画像コンテンツモデレーションの専門家です。 +与えられた画像を分析し、不適切な内容のカテゴリーとその深刻度を0から1の範囲で評価してください。 + +## 注意点 +以下に特に注意してください +1. 画像内の細かいディテールや暗示的な表現にも注意してください。隠された意味や背景の文脈を考慮に入れてください。 +2. 文化的な文脈や社会的背景を考慮し、日本社会における受け止められ方を反映してください。 +3. 画像の内容が年齢や社会的立場によって異なる解釈を引き起こす場合、それも考慮してください。 + +# 評価方法 +各カテゴリーのスコアは以下のガイドラインに従って評価してください。 +0.0: 問題なし +0.1-0.2: 軽度の懸念あり +0.3-0.5: 受け手によっては不快または問題があると感じられる可能性あり +0.6-0.8: 中程度の有害性があり、問題視される可能性が高い +0.9-1.0: 深刻な有害性があり、強い懸念や行動が必要な状態 + +カテゴリーごとのスコアを計算し、'category_scores'オブジェクトに格納してください。 +` +// 与えられたテキストを英語で考えて、日本語で回答してください。 + +/** + * テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するサービスのインターフェース + * + * このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。 + * 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。 + * + * @interface IGuardianImageService + * @method guardianImage - 指定されたテキストを分析し、不適切な内容の有無を判定します + * @param image - 分析対象の画像(Base64形式) + * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 + */ +export interface IGuardianImageService { + guardianImage(image: string): Promise +} + +/** + * テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するサービスの実装 + * + * このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。 + * 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。 + * + * @class GuardianService + * @implements IGuardianService + * @param openai - OpenAIProviderのインスタンス + * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します + * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 + */ +export class GuardianImageService implements IGuardianImageService { + constructor(private openai: OpenAIProvider) {} + + async guardianImage(image: string): Promise { + try { + const { object } = await generateObject({ + model: this.openai('gpt-4o-mini'), + schema: z.object({ category_scores: categoryScoresSchema }), + messages: [ + { role: 'system', content: systemPrompt }, + { + role: 'user', + content: [{ type: 'image', image }], + }, + ], + }) + return object.category_scores + } catch (error) { + console.error(error) + throw new InternalServerError('Failed to analyze image') + } + } +} diff --git a/apps/api/src/features/guardians/guardianImage.usecase.ts b/apps/api/src/features/guardians/guardianImage.usecase.ts new file mode 100644 index 0000000..239cb90 --- /dev/null +++ b/apps/api/src/features/guardians/guardianImage.usecase.ts @@ -0,0 +1,71 @@ +import { + GuardianImageInput, + GuardianResult, +} from '@peace-net/shared/types/guardian' +import { failure, success, type Result } from '@peace-net/shared/utils/result' + +import { + checkFlagged, + createCategories, +} from '~/features/guardians/guardian.utils' +import { IGuardianImageService } from '~/features/guardians/guardianImage.service' +import { IUsageFacade } from '~/features/usages/usage.facade' + +/** + * テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するユースケースのインターフェース + * + * このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。 + * また、使用回数をカウントします。 + * + * @interface IGuardianImageUseCase + * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します + * @param dto - 分析対象のテキストとスコア閾値 + * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 + */ +export interface IGuardianImageUseCase { + guardianImage(input: GuardianImageInput): Promise> +} + +/** + * テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するユースケース + * + * このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。 + * また、使用回数をカウントします。 + * + * @class GuardianImageUseCase + * @implements IGuardianImageUseCase + * @param guardianImageService - GuardianServiceのインスタンス + * @method guardianImage - 指定されたテキストを分析し、不適切な内容の有無を判定します + * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 + */ +export class GuardianImageUseCase implements IGuardianImageUseCase { + constructor( + private guardianImageService: IGuardianImageService, + private usageFacade: IUsageFacade, + ) {} + + async guardianImage( + input: GuardianImageInput, + ): Promise> { + try { + const { image, score_threshold = 0.5, userId, apiKeyId } = input + + const categoryScores = + await this.guardianImageService.guardianImage(image) + + const flagged = checkFlagged(categoryScores, score_threshold) + + const categories = createCategories(categoryScores, score_threshold) + + await this.usageFacade.incrementUsage(userId, apiKeyId, 'guardians') + + return success({ + flagged, + categories, + category_scores: categoryScores, + }) + } catch (error) { + return failure(error) + } + } +} diff --git a/apps/api/src/features/guardians/guardian.controller.ts b/apps/api/src/features/guardians/guardianText.controller.ts similarity index 77% rename from apps/api/src/features/guardians/guardian.controller.ts rename to apps/api/src/features/guardians/guardianText.controller.ts index 2d60bf6..c6a8bfd 100644 --- a/apps/api/src/features/guardians/guardian.controller.ts +++ b/apps/api/src/features/guardians/guardianText.controller.ts @@ -2,22 +2,21 @@ import { NotImplementedError } from '@peace-net/shared/core/error' import { GuardianTextDTO } from '@peace-net/shared/types/guardian' import { Context } from 'hono' -import { IGuardianUseCase } from '~/features/guardians/guardian.usecase' +import { IGuardianTextUseCase } from '~/features/guardians/guardianText.usecase' /** * テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するコントローラー * * このコントローラーは、GuardianUseCaseを使用してテキストの不適切な内容を分析し、結果をクライアントに返します。 * - * @class GuardianController + * @class GuardianTextController * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します - * @method guardianImage - 指定された画像を分析し、不適切な内容の有無を判定します - * @param guardianUseCase - GuardianUseCaseのインスタンス + * @param guardianTextUseCase - GuardianUseCaseのインスタンス * @returns 分析結果を含むJSONレスポンス * @throws エラーが発生した場合はエラーレスポンスを返します */ -export class GuardianController { - constructor(private guardianUseCase: IGuardianUseCase) {} +export class GuardianTextController { + constructor(private guardianUseCase: IGuardianTextUseCase) {} async guardianText(c: Context) { const dto = (await c.req.json()) as GuardianTextDTO diff --git a/apps/api/src/features/guardians/guardian.service.ts b/apps/api/src/features/guardians/guardianText.service.ts similarity index 95% rename from apps/api/src/features/guardians/guardian.service.ts rename to apps/api/src/features/guardians/guardianText.service.ts index 54b710a..65a4327 100644 --- a/apps/api/src/features/guardians/guardian.service.ts +++ b/apps/api/src/features/guardians/guardianText.service.ts @@ -55,12 +55,12 @@ const systemPrompt = ` * このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。 * 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。 * - * @interface IGuardianService + * @interface IGuardianTextService * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します * @param text - 分析対象のテキスト * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 */ -export interface IGuardianService { +export interface IGuardianTextService { guardianText(text: string, selectedModel: Models): Promise } @@ -70,13 +70,13 @@ export interface IGuardianService { * このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。 * 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。 * - * @class GuardianService - * @implements IGuardianService + * @class GuardianTextService + * @implements IGuardianTextService * @param openai - OpenAIProviderのインスタンス * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 */ -export class GuardianService implements IGuardianService { +export class GuardianTextService implements IGuardianTextService { constructor( private openai: OpenAIProvider, private anthropic: AnthropicProvider, diff --git a/apps/api/src/features/guardians/guardian.usecase.ts b/apps/api/src/features/guardians/guardianText.usecase.ts similarity index 85% rename from apps/api/src/features/guardians/guardian.usecase.ts rename to apps/api/src/features/guardians/guardianText.usecase.ts index fb0aabe..3a94562 100644 --- a/apps/api/src/features/guardians/guardian.usecase.ts +++ b/apps/api/src/features/guardians/guardianText.usecase.ts @@ -4,11 +4,11 @@ import { } from '@peace-net/shared/types/guardian' import { failure, success, type Result } from '@peace-net/shared/utils/result' -import { IGuardianService } from '~/features/guardians/guardian.service' import { checkFlagged, createCategories, } from '~/features/guardians/guardian.utils' +import { IGuardianTextService } from '~/features/guardians/guardianText.service' import { IUsageFacade } from '~/features/usages/usage.facade' /** @@ -17,12 +17,12 @@ import { IUsageFacade } from '~/features/usages/usage.facade' * このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。 * また、使用回数をカウントします。 * - * @interface IGuardianUseCase + * @interface IGuardianTextUseCase * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します * @param dto - 分析対象のテキストとスコア閾値 * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 */ -export interface IGuardianUseCase { +export interface IGuardianTextUseCase { guardianText(input: GuardianTextInput): Promise> } @@ -32,15 +32,15 @@ export interface IGuardianUseCase { * このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。 * また、使用回数をカウントします。 * - * @class GuardianUseCase - * @implements IGuardianUseCase + * @class GuardianTextUseCase + * @implements IGuardianTextUseCase * @param guardianService - GuardianServiceのインスタンス * @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します * @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。 */ -export class GuardianUseCase implements IGuardianUseCase { +export class GuardianTextUseCase implements IGuardianTextUseCase { constructor( - private guardianService: IGuardianService, + private guardianTextService: IGuardianTextService, private usageFacade: IUsageFacade, ) {} @@ -56,7 +56,7 @@ export class GuardianUseCase implements IGuardianUseCase { apiKeyId, } = input - const categoryScores = await this.guardianService.guardianText( + const categoryScores = await this.guardianTextService.guardianText( text, model, ) diff --git a/packages/shared/src/schemas/guardian.ts b/packages/shared/src/schemas/guardian.ts index 44cf7a5..207d593 100644 --- a/packages/shared/src/schemas/guardian.ts +++ b/packages/shared/src/schemas/guardian.ts @@ -16,7 +16,7 @@ export const guardianTextRequestSchema = z.object({ }) export const guardianImageRequestSchema = z.object({ - image_url: z.string(), + image: z.string(), score_threshold: z.number().max(1).min(0).optional().default(0.5), }) diff --git a/packages/shared/src/types/guardian.ts b/packages/shared/src/types/guardian.ts index 94b027b..0f3bbd4 100644 --- a/packages/shared/src/types/guardian.ts +++ b/packages/shared/src/types/guardian.ts @@ -19,4 +19,9 @@ export type GuardianTextInput = GuardianTextDTO & { export type GuardianImageDTO = z.infer +export type GuardianImageInput = GuardianImageDTO & { + userId: string + apiKeyId: string +} + export type GuardianResult = z.infer