Auto-Writing NSFW Descriptions

#The Challenge

# I recently noticed an old email I got from a mastodon server I’m on asking me to be more specific about the “NSFW” content-warnings I had on some of my posts, saying that they should be more specific to describe why each post is NSFW. I happen to agree. But is there a way to automate this? Challenge accepted!  

Challenge accepted!

#The Pieces Emerge

# … so I manually went back and added more details to all of my posts, but this whole thing has gotten me brainstorming about ways to try to automate adding a short but detailed ContentWarning description.  

manually edited cw

# Going back and adding all the details manually has helped me figure out what descriptions I tend to need.  

list of manual descriptions

#The Theory

# My hunch is that it should theoretically be possible for a program to accurately guess what the description should be based on the #hashtags. I might need to add a few new tags and a real-time preview of the description to ensure the keywords I’ve added are enough to adequately describe the post. The goal is to generate the description automatically without requiring extra work from me. I’ve already been adding keywords, so it’s just a matter of adding the right ones.  

Let’s just use the hashtags

# More extreme descriptions should replace milder ones, so “suggestive content” should get replaced by “lesbian sex”. I would want to replace redundant descriptions anyway since screen space is limited.

warnings-need-to-be-short

# Maybe a NSFW hashtag could just default to “sexual female nudity” as the description, and then just replace it if a more specific keyword is used? This approach is appealingly elegant, and would probably be adequate for the kind of stuff I post.

# I could always just have the option to manually write a description if the program’s guess is way off.  

brainstorming

# explicit warningThen again, it is possible for a warning to be too detailed. After all, seeing the words “dog fucking girl” would be shocking for a lot of people. So a milder yet still accurate phrase might be better? Maybe something like “sexual content, animal”  

# Besides, it might be useful to separate some words out of the description like that. I’ve already been using emoji’s to label some recurring concepts. I think it makes it faster and easier for people to recognize the themes without actually having to read all the words.

# Icons also have another surprising benefit. They cross language barriers!

# I have often said that every icon should have a text label because there’s no guarantee that people will know what an icon means by itself. But I’m starting to think the opposite is also true.

# After all, if you saw this label in a content warning, what does it even mean?
性 獣姦  

# But what about now?
🔞 性 🐕 獣姦

# Even if you can’t read the language, you know this has something to do with animals, and it’s naughty.  

#The Details Emerge

# I intend to use my keywords in combination to describe things, but first I need to figure out what descriptions each one will generate individually. 

# descriptions possible using old keywords Something fun to look at if you get tired of screenshots of text

# I realized I might need to add a few more keywords to indicate all the descriptions I want.  

# new keywords needed Making tiny adjustments

# Now let’s try re-creating the old descriptions for my projects. Which set of keywords would theoretically create those descriptions? With the new keywords added, this mostly looks possible.  

# rewriting descriptions using new keywords rehearsal

# … and then look at the most common keyword combinations to figure out which tags I actually need.  

# most common keywords needed filtering

#Planning to Accomplish Something

# Now to plan the actual behavior of my program. Let’s list every keyword combination I think I need, the replacement behavior, and which tags should trigger a content warning.  

# keyword combo plans planning

# Hmm… I think I’ll lay out the description kind of like this.
An optional adjective like “nude”, then the action, followed by “female” or “girl”
And maybe start thinking about how to store this information.

# Simply combining keywords and generating descriptions is easy enough. And that would technically be adequate, but let’s try to combine or replace things to make it read better.  

# template and settings replace python

#By Jove I’ve Solved It!

# I had a brilliant idea!I took a break to pick up some groceries, and while walking I had a brilliant idea. Simple text replacement!

Multiple replacements happening in-series.

I could store a list of rules in the settings and have the program apply them in order. Each rule checks for certain keywords and makes adjustments to the description. This works because each rule is simple and only needs to be aware of its own changes. Text-replacement causes the effects of multiple rules to combine together. Priority is just a matter of what order the rules get applied.  

text replacement technique

# The idea is each rule checks for a set of tags. If they’re all present, then the rule applies, which means it searches for a piece of text in the description, and then replaces it with different text.

# So for example, with the tags “nude, girl, lesbians” …

  1. “lesbians” adds the text “lesbian sex with a woman”
  2. “nude” replaces “woman” with “nude woman”
  3. “girl” replaces “woman” with “girl”

# … and the result is “lesbian sex with a nude girl”

# If there is no text to search for, the rule simply adds text at the end of the description. If that text already exists it won’t be added again.

# A keyword can be excluded using !keyword, meaning a rule only applies if that keyword is not being used. This helps prevent two similar descriptions from being added, and allows some rules to be mutually exclusive. (one apples, but never both)

# You can also make adjustments to the grammar by not requiring any particular keywords, and just looking for certain text combinations to replace.  

# planned rewrite rules Let’s put this sucker together

#It Lives!!

# I put together a prototype program to test my idea for an automatic NSFW description writer. And it works!  

# And the final test. I looked at my existing posts and tried to generate the right descriptions for them using this program. Each one only needed a few keywords. Not all of them were perfect but most of them were.

# I’d call this a success!  

# … aaaaaaand done!

# Automatic NSFW descriptions added automatically with no extra work. It only took, like, a week of work. Ah the joys of being a programmer.

nsfw-description-in-cross-poster nsfw-description-test-post

# I probably won’t be using this on my website because its custom pairing icons are compact and already have detailed descriptions.

pairing-descriptions

# Download JavaScript Code


/*
VERSION:  1.1
AUTHOR:   Humbird0
LICENSE:  Public Domain

DESCRIPTION:
  Creates a human-readable description based on a set of keywords.
  
USAGE:
  // read selected keywords_ary, and store each item in an object-list  {"key1":true, "key2":true, "key3":true}
  var tags_obj = {};
  for(var i=0; i<keywords_ary.length; i++){
    tags_obj[  keywords_ary[ i ]  ] = true;
  }
  // generate a description
  var description_txt = getDescriptionFromKeywords( tags_obj, rules_ary );
  // display this description
  console.log( description_txt );
*/


/**
* Creates a human-readable description based on a set of keywords.
* 
* @param  keywords  an object-list of keywords to evaluate  {key1:true, key2:true, key3:true}
* @param  rules     an array of rules that generate and modify the description  [{hasKeywords:["key1","key2"], replaceText:"", withText:""}]
* @return           a string description
*/
function getDescriptionFromKeywords( keywords_obj, rules_ary, startWithText ){
  // write a description, by applying each rule
  var description_txt = startWithText || "";
  for(var r=0; r<rules_ary.length; r++){
    var thisRule = rules_ary[ r ];
    var ruleApplies = true;
    var keyLen = thisRule.hasKeywords.length || 0;
    for(var k=0; k<keyLen; k++){
      var isForbidden = (thisRule.hasKeywords[k].charAt(0) === "!");
      if( isForbidden === false ){
        // No keywords  >>  this means always apply this rule
				if( thisRule.hasKeywords[k] === "" ){
					continue;
				}
				
				// This is a regular keyword, so it must be present. If this keyword is missing, then this rule should be skipped.
        if( keywords_obj[  thisRule.hasKeywords[k]  ] === undefined ){
          ruleApplies = false;
          break;// stop: for keyLen   // stop checking any more keywords
        }// if this required keyword exists
      }else{
        // if isForbidden === true
        // This is a forbidden keyword, so it must be absent. If this keyword is present, then this rule should be skipped.
        if( keywords_obj[  thisRule.hasKeywords[k].substr(1)  ] !== undefined ){
          ruleApplies = false;
          break;// stop: for keyLen   // stop checking any more keywords
        }// if this forbidden keyword is present
      }
    }// for keyLen
    //
    if( ruleApplies === false ){
      continue;// skip to next: for rules_ary   // skip this rule
    }
    // else,  ruleApplies === true
    // is this rule replacing text?
    var replaceText = thisRule.replaceText || "";
    var withText = thisRule.withText || "";
    if( replaceText ){
      // this rule is replacing text
      var hasText = ( description_txt.indexOf( replaceText ) > -1 );
      if( hasText ){
        // replaceText is found...
        // ... so replace all instances of replaceText with withText.
        description_txt = description_txt.split( replaceText ).join( withText );
      }else{
        // replaceText is missing...
        // ... so do nothing.
        continue;// skip to next: for rules_ary   // skip this rule
      }
    }else{
      // this rule is adding text
      var alreadyHasNewText = (description_txt.indexOf( withText ) > -1);
      if( alreadyHasNewText ){
        // withText already exists in description_txt...
        // ... so do nothing.
        continue;// skip to next: for rules_ary   // skip this rule
      }else{
        // withText is absent from description_txt...
        /// ... so append withText to the end of description_txt.
        description_txt += withText;       
      }
    }
  }// for each rule
  // remove ", " from the start of description_txt
  if( description_txt.indexOf(", ") === 0 )
    description_txt = description_txt.substr( 2 );
  // output
  return description_txt;
}// getDescriptionFromKeywords()

# Here’s a live demonstration: