Created
January 31, 2026 20:17
-
-
Save tinbotu/8236e48ff1b3bf3857130e13d8507ad7 to your computer and use it in GitHub Desktop.
フィクション作中作向けツール: 人間にとってランダム感のあるランダムパスワード生成 (NEVER USE THIS TO GENERATE **REALPASSWORD** !!!)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Pseudo Password Generator | |
| A tool to generate strings that "look random" to the human eye for use in fiction. | |
| Prioritizes visual randomness over statistical randomness. | |
| """ | |
| import argparse | |
| import math | |
| import random | |
| import sys | |
| from collections import Counter | |
| from typing import Tuple | |
| # Available characters | |
| CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" | |
| # Characters allowed for the first position (letters only) | |
| CHARSET_FIRST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
| # Vowels (cause strings to look too readable) | |
| VOWELS = set("aeiouAEIOU") | |
| # Keyboard layout sequences | |
| KEYBOARD_SEQUENCES = [ | |
| "qwerty", "asdf", "zxcv", "qwer", "asdfgh", "zxcvbn", | |
| "yuiop", "hjkl", "bnm", "qazwsx", "edcrfv", "tgbyhn", | |
| ] | |
| # Common sequences | |
| COMMON_SEQUENCES = [ | |
| "abc", "bcd", "cde", "def", "efg", "fgh", "ghi", "hij", "ijk", "jkl", | |
| "klm", "lmn", "mno", "nop", "opq", "pqr", "qrs", "rst", "stu", "tuv", | |
| "uvw", "vwx", "wxy", "xyz", | |
| "012", "123", "234", "345", "456", "567", "678", "789", "890", | |
| "321", "432", "543", "654", "765", "876", "987", | |
| ] | |
| # Leet speak mappings (patterns to avoid) | |
| LEET_PATTERNS = [ | |
| ("a", "4"), ("e", "3"), ("i", "1"), ("o", "0"), ("s", "5"), | |
| ("t", "7"), ("b", "8"), ("g", "9"), ("l", "1"), | |
| ] | |
| def has_consecutive_chars(s: str, max_repeat: int = 2) -> bool: | |
| """Check if the string contains consecutive identical characters.""" | |
| for i in range(len(s) - max_repeat + 1): | |
| if len(set(s[i:i + max_repeat])) == 1: | |
| return True | |
| return False | |
| def has_sequence(s: str) -> bool: | |
| """Check if the string contains keyboard or common sequences.""" | |
| s_lower = s.lower() | |
| for seq in KEYBOARD_SEQUENCES + COMMON_SEQUENCES: | |
| if seq in s_lower: | |
| return True | |
| return False | |
| def is_palindrome_like(s: str, min_len: int = 4) -> bool: | |
| """Check if the string contains palindromic patterns.""" | |
| s_lower = s.lower() | |
| for length in range(min_len, len(s) + 1): | |
| for i in range(len(s) - length + 1): | |
| substr = s_lower[i:i + length] | |
| if substr == substr[::-1]: | |
| return True | |
| return False | |
| def has_pronounceable_pattern(s: str, max_syllables: int = 3) -> bool: | |
| """Check if the string contains pronounceable patterns (consonant-vowel sequences).""" | |
| s_lower = s.lower() | |
| # Count consecutive consonant-vowel patterns | |
| cv_count = 0 | |
| i = 0 | |
| while i < len(s_lower) - 1: | |
| char = s_lower[i] | |
| next_char = s_lower[i + 1] | |
| if char.isalpha() and next_char.isalpha(): | |
| if char not in VOWELS and next_char in VOWELS: | |
| cv_count += 1 | |
| i += 2 | |
| continue | |
| cv_count = 0 | |
| i += 1 | |
| if cv_count >= max_syllables: | |
| return True | |
| return False | |
| def has_leet_pattern(s: str) -> bool: | |
| """Check if the string contains leet speak patterns.""" | |
| s_lower = s.lower() | |
| # Check if typical leet substitutions are adjacent | |
| for i in range(len(s) - 1): | |
| for letter, digit in LEET_PATTERNS: | |
| # Letter and its corresponding digit are adjacent | |
| pair = s_lower[i:i + 2] | |
| if pair == letter + digit or pair == digit + letter: | |
| return True | |
| return False | |
| def has_too_many_vowels(s: str, max_ratio: float = 0.4) -> bool: | |
| """Check if the string has too many vowels, making it too readable.""" | |
| alpha_chars = [c for c in s if c.isalpha()] | |
| if not alpha_chars: | |
| return False | |
| vowel_count = sum(1 for c in alpha_chars if c in VOWELS) | |
| return vowel_count / len(alpha_chars) > max_ratio | |
| def has_visual_similarity_cluster(s: str) -> bool: | |
| """Check if the string contains clusters of visually similar characters.""" | |
| similar_groups = [ | |
| set("Il1|"), | |
| set("O0"), | |
| set("S5"), | |
| set("Z2"), | |
| set("B8"), | |
| set("G6"), | |
| ] | |
| for i in range(len(s) - 2): | |
| window = s[i:i + 3] | |
| for group in similar_groups: | |
| matches = sum(1 for c in window if c in group) | |
| if matches >= 2: | |
| return True | |
| return False | |
| def is_visually_random(s: str) -> bool: | |
| """Comprehensive check if the string appears visually random.""" | |
| if has_consecutive_chars(s): | |
| return False | |
| if has_sequence(s): | |
| return False | |
| if is_palindrome_like(s): | |
| return False | |
| if has_pronounceable_pattern(s): | |
| return False | |
| if has_leet_pattern(s): | |
| return False | |
| if has_too_many_vowels(s): | |
| return False | |
| if has_visual_similarity_cluster(s): | |
| return False | |
| return True | |
| def generate_candidate(length: int) -> str: | |
| """Generate a candidate string.""" | |
| if length < 1: | |
| return "" | |
| # First character must be a letter | |
| first_char = random.choice(CHARSET_FIRST) | |
| rest = ''.join(random.choice(CHARSET) for _ in range(length - 1)) | |
| return first_char + rest | |
| def calculate_entropy(s: str) -> float: | |
| """ | |
| Calculate the Shannon entropy of a string (in bits). | |
| Computes information content based on character frequency. | |
| """ | |
| if not s: | |
| return 0.0 | |
| counter = Counter(s) | |
| length = len(s) | |
| entropy = 0.0 | |
| for count in counter.values(): | |
| probability = count / length | |
| entropy -= probability * math.log2(probability) | |
| # Total entropy of the string (in bits) | |
| return entropy * length | |
| def calculate_max_entropy(length: int, charset_size: int = len(CHARSET)) -> float: | |
| """ | |
| Calculate the theoretical maximum entropy. | |
| This is the entropy when all characters appear with equal probability. | |
| """ | |
| # Entropy per character = log2(charset_size) | |
| # Total entropy = log2(charset_size) * length | |
| return math.log2(charset_size) * length | |
| def get_entropy_stats(s: str) -> Tuple[float, float, float]: | |
| """ | |
| Get entropy statistics. | |
| Returns: (actual entropy, maximum entropy, percentage) | |
| """ | |
| actual = calculate_entropy(s) | |
| maximum = calculate_max_entropy(len(s)) | |
| percentage = (actual / maximum * 100) if maximum > 0 else 0.0 | |
| return actual, maximum, percentage | |
| def generate_visually_random_string(length: int, max_attempts: int = 2000) -> str: | |
| """Generate a visually random string.""" | |
| for _ in range(max_attempts): | |
| candidate = generate_candidate(length) | |
| if is_visually_random(candidate): | |
| return candidate | |
| # Worst case: return the last candidate with a warning | |
| print(f"Warning: Could not generate ideal string after {max_attempts} attempts", | |
| file=sys.stderr) | |
| return candidate | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Generate strings that look random to the human eye for use in fiction' | |
| ) | |
| parser.add_argument( | |
| 'length', | |
| type=int, | |
| nargs='?', | |
| default=16, | |
| help='length of the generated string (default: 16)' | |
| ) | |
| parser.add_argument( | |
| 'count', | |
| type=int, | |
| nargs='?', | |
| default=10, | |
| help='number of strings to generate (default: 10)' | |
| ) | |
| args = parser.parse_args() | |
| length = args.length | |
| count = args.count | |
| if not (8 <= length <= 32): | |
| print("Warning: length should be between 8 and 32", file=sys.stderr) | |
| if count < 1: | |
| parser.error("count must be at least 1") | |
| max_entropy = calculate_max_entropy(length) | |
| print(f"# Charset: {len(CHARSET)} chars, Length: {length}, Max Entropy: {max_entropy:.2f} bits") | |
| print() | |
| for _ in range(count): | |
| result = generate_visually_random_string(length) | |
| actual, maximum, percentage = get_entropy_stats(result) | |
| print(f"{result} [{actual:.1f}/{maximum:.1f} bits = {percentage:.1f}%]") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment