Discourse AI - NSFW

:bookmark: 이 주제는 Discourse AI 플러그인의 NSFW(Not Safe For Work) 기능 설정에 대해 다룹니다.

:person_raising_hand: 필요한 사용자 권한: 관리자

NSFW 모듈은 Discourse 인스턴스의 게시물 및 채팅 메시지에 업로드되는 모든 새 이미지의 NSFW 점수를 자동으로 분류할 수 있습니다. 이 시스템은 전문적이거나 공개적인 환경에서 부적절하거나 노골적일 수 있는 콘텐츠를 식별하고 관리하는 데 도움이 됩니다.

임계값을 초과하는 콘텐츠에 대한 자동 플래깅(신고)도 활성화할 수 있습니다.

설정

  • ai_nsfw_detection_enabled: 모듈을 활성화하거나 비활성화합니다.

  • ai_nsfw_inference_service_api_endpoint: 모듈이 사용하는 API가 실행 중인 URL입니다. CDCK 호스팅을 사용하는 경우 이 설정은 자동으로 처리됩니다. 자체 호스팅(self-hosting)을 사용하는 경우 자체 호스팅 가이드를 확인하세요.

  • ai_nsfw_inference_service_api_key: 위에서 설정한 API의 API 키입니다. CDCK 호스팅을 사용하는 경우 이 설정은 자동으로 처리됩니다. 자체 호스팅(self-hosting)을 사용하는 경우 자체 호스팅 가이드를 확인하세요.

  • ai_nsfw_models: 이미지 분류에 사용할 모델입니다. 임계값이 다른 두 가지 모델을 제공합니다:

    • opennsfw2: 0.0과 1.0 사이의 단일 점수를 반환합니다. 업로드가 NSFW로 간주되기 위한 임계값은 ai_nsfw_flag_threshold_general 설정을 통해 설정합니다.
    • nsfw_detector: drawings, hentai, porn, sexy 네 가지 범위에 대한 점수를 반환합니다. 각 범위의 임계값은 각각의 ai_nsfw_flag_threshold_${category} 설정을 통해 설정합니다. 일반 임계값이 이 중 어떤 값보다 낮은 경우, 해당 콘텐츠는 NSFW로 간주됩니다.
  • ai_nsfw_flag_automatically: 설정된 임계값을 초과하는 게시물/채팅 메시지를 자동으로 플래깅합니다. 이 설정이 비활성화된 경우, 분류 결과는 데이터베이스에 저장만 됩니다.

추가 자료

11개의 좋아요

감사합니다.

이미지를 업로드한 후 nsfw 서비스를 사용할 때 이 문제를 발견했습니다. 이미지 저장에는 S3를 사용하는데, 이것이 관련이 있는지 모르겠습니다.

INFO:     10.0.2.145:23412 - "POST /api/v1/classify HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/anyio/streams/memory.py", line 94, in receive
    return self.receive_nowait()
  File "/usr/local/lib/python3.9/dist-packages/anyio/streams/memory.py", line 89, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 77, in call_next
    message = await recv_stream.receive()
  File "/usr/local/lib/python3.9/dist-packages/anyio/streams/memory.py", line 114, in receive
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/uvicorn/protocols/http/h11_impl.py", line 404, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/usr/local/lib/python3.9/dist-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/usr/local/lib/python3.9/dist-packages/starlette_exporter/middleware.py", line 289, in __call__
    await self.app(scope, receive, wrapped_send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/cors.py", line 84, in __call__
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 106, in __call__
    response = await self.dispatch_func(request, call_next)
  File "./app.py", line 49, in dispatch
    response = await call_next(request)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 80, in call_next
    raise app_exc
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 69, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/usr/local/lib/python3.9/dist-packages/starlette/routing.py", line 706, in __call__
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "./app.py", line 79, in classify
    results = models[parsed_params.model](tfile)
  File "/usr/local/lib/python3.9/dist-packages/opennsfw2/_inference.py", line 31, in predict_image
    pil_image = Image.open(image_path)
  File "/usr/local/lib/python3.9/dist-packages/PIL/Image.py", line 3283, in open
    raise UnidentifiedImageError(msg)
PIL.UnidentifiedImageError: cannot identify image file '/tmp/tmpbqq82655'
ERROR:uvicorn.error:Exception in ASGI application
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/anyio/streams/memory.py", line 94, in receive
    return self.receive_nowait()
  File "/usr/local/lib/python3.9/dist-packages/anyio/streams/memory.py", line 89, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 77, in call_next
    message = await recv_stream.receive()
  File "/usr/local/lib/python3.9/dist-packages/anyio/streams/memory.py", line 114, in receive
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/uvicorn/protocols/http/h11_impl.py", line 404, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/usr/local/lib/python3.9/dist-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/usr/local/lib/python3.9/dist-packages/starlette_exporter/middleware.py", line 289, in __call__
    await self.app(scope, receive, wrapped_send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/cors.py", line 84, in __call__
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 106, in __call__
    response = await self.dispatch_func(request, call_next)
  File "./app.py", line 49, in dispatch
    response = await call_next(request)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 80, in call_next
    raise app_exc
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/base.py", line 69, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/usr/local/lib/python3.9/dist-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/usr/local/lib/python3.9/dist-packages/starlette/routing.py", line 706, in __call__
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/dist-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "./app.py", line 79, in classify
    results = models[parsed_params.model](tfile)
  File "/usr/local/lib/python3.9/dist-packages/opennsfw2/_inference.py", line 31, in predict_image
    pil_image = Image.open(image_path)
  File "/usr/local/lib/python3.9/dist-packages/PIL/Image.py", line 3283, in open
    raise UnidentifiedImageError(msg)
PIL.UnidentifiedImageError: cannot identify image file '/tmp/tmpbqq82655'

Sidekiq에서는 다음과 같이 보입니다:

제가 무엇을 잘못하고 있는 건가요?

이 문제는 S3와 관련이 있을 것 같습니다. 이미지를 업로드할 때 에러 로그에 다음과 같은 오류가 표시됩니다:
S3 URL은 삭제되었습니다…

즉, 플러그인이 이미지를 너무 일찍 로드하려 하거나, 기타 이유로 이미지를 찾지 못하는 것으로 보입니다.

저는 S3에 CDN을 사용하거나 로컬 스토리지를 사용하는 사이트에서만 테스트했습니다. Object Storage를 사용하는 경우, 그 앞에 CDN을 사용하는 것을 권장합니다.

1개의 좋아요

네, 저희가 정확히 그렇게 하고 있습니다 - AWS CloudFront를 사용 중입니다. 스택은 https://github.com/aws-samples/aws-cdk-for-discourse에 기반하고 있습니다. 디버깅을 시도해서 결과를 공유하겠습니다.

2개의 좋아요

API 키는 어디서 받아야 하나요?

관리자 패널 아래에 있어야 하지 않나요?

이 기능은 제게 너무 민감하게 반응하는 것 같습니다. 너무 자주 플래그가 걸리거든요. 그냥 사람 사진만으로도 NSFW로 분류됩니다.

현재 임계값(threshold)을 60으로 설정해 두었는데, 이 수치를 높여야 하는지 낮춰야 하는지, 그리고 한계가 무엇인지 명확하지 않습니다. 100으로 설정하면 사실상 기능을 비활성화하는 것과 같은 효과가 있나요? 이에 대한 문서화가 거의 없는 것 같습니다.

또한, 독성(toxicity) 기능에서는 특정 역할을 대상으로 완전히 비활성화할 수 있는 옵션이 있는데, 왜 이 기능에는 그런 옵션이 없는지 궁금합니다. 저희 스태프들이 무례한 내용을 게시할 것이라고는 거의 생각하지 않는데, AI가 그들의 게시물을 계속 플래그 처리해서 늘 갈등이 생기고 있습니다. 이제 저희 포럼에서는 이게 거의 농담거리가 될 정도입니다.

opennsfw2를 사용 중입니다.

1개의 좋아요

불행히도 이러한 구형 ML 모델은 유연성이 부족하며, 자체 인스턴스를 실행하는 데 필요한 사양은 낮지만, Discourse를 사용하는 다양한 커뮤니티에 있어 그 엄격함이 너무 큰 장애물이 된다는 것을 발견했습니다.

현재 계획은 이러한 구형 모델에 의존하는 모든 기능을 비추천(deprecate) 처리하고, 프롬프트를 조정하여 필요에 더 잘 맞도록 할 수 있는 현대적인 LLM으로 모든 것을 마이그레이션하는 것입니다.

1개의 좋아요

이 부분에 대해 어떤 변화가 있었나요? NSFW 감지기는 여전히 매우 부정확해서, 사람 사진 대부분에서 오작동합니다.

네, What’s next for NSFW detection in Discourse AI에서 우리의 계획을 공유한 바 있습니다. 결론부터 말씀드리면, 훨씬 더 높은 정확도와 유연성을 위해 Discourse AI Post Classifier - Automation rule을 사용하실 수 있습니다.

비전 LLM을 통한 NSFW 사용 사례에 대한 평가를 수행했으며 훌륭한 결과를 얻었습니다. 이에 따라 현재 엔터프라이즈 고객사 대상으로 해당 기능을 배포하고 있으며, 몇 주 안에 관련 가이드를 공개할 예정입니다.

2개의 좋아요