Files
carkov/carkov/__main__.py
T
cassowary d6e329ff39 Bump to 0.2.0
- Add Unweighted mode to command line
- Download nltk data if needed.
2023-11-04 20:40:03 -07:00

173 lines
6.1 KiB
Python

#
# carkov markov chain library
# © Copyright 2021 by Aldercone Studio <alderconestudio@gmail.com>
# This is free software, see the included LICENSE for terms and conditions.
#
# This module provides a command line interface to doing some common operations.
#
import argparse
import enum
import pathlib
import sys
import traceback
from random import Random
from typing import cast
from .analyze.abstract import AbstractAnalyzer
from .analyze.english import English
from .analyze.words import Words
from .chain import Chain, from_analyzer
from .serializer import dump_chainer, load_chainer
from .utils import make_sent, make_word
ERROR_NO_FILE = 1
ERROR_EXISTING_OUTPUT = 2
ERROR_WRONG_ORDER = 3
ERROR_WRONG_CLASS = 4
class AnalyzeMode(enum.Enum):
english = "english"
word = "word"
def __str__(self) -> str:
return self.value
JOINERS = {"Words": make_word, "English": make_sent}
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="python -mcarkov",
description=("Process a text corpus in a markov chain fashion and/or output from"
"an analysis."))
subparsers = parser.add_subparsers(dest='command')
analyze_sub = subparsers.add_parser('analyze', help="Analyze a corpus")
analyze_sub.add_argument('output', help="Output chain to specified destination", type=pathlib.Path)
analyze_sub.add_argument('input', help="The corpus to analyze", type=pathlib.Path, nargs='+')
overappend = analyze_sub.add_mutually_exclusive_group()
overappend.add_argument('-o', '--overwrite', help='Overwrite output file.', action='store_true')
overappend.add_argument('-a', '--append', help='Append output file.', action='store_true')
analyze_sub.add_argument('-w', '--window', help='Select length of analysis window', type=int, default=2)
analyze_sub.add_argument('-m',
'--mode',
help='Select analyzis mode',
type=AnalyzeMode,
choices=list(AnalyzeMode),
default=AnalyzeMode.english)
analyze_sub.add_argument('-t', '--test', help="Output a sample from the chainer generated", action='store_true')
chain_sub = subparsers.add_parser('chain', help="Output from a chainer")
chain_sub.add_argument('input', help="The chain file to load", type=pathlib.Path)
chain_sub.add_argument('-u', '--unweighted', help="Don't weight token selection by statistical frequency.", action='store_true')
chain_sub.add_argument('-c', '--count', help="Number of chain outputs to output", type=int, default=10)
chain_sub.add_argument('-m', '--maxlen', help="Maximum length in tokens of output (0 is unlimited)", type=int, default=0)
return parser.parse_args()
def print_chainer_output(chainer: Chain, random_state: Random, weighted: bool = False):
if chainer.analyzer_class in JOINERS:
print(JOINERS[cast(str, chainer.analyzer_class)](chainer.walk(random_state, weighted)))
else:
print(chainer.walk(random_state, weighted))
def command_analyze(args: argparse.Namespace) -> int:
if not any(x.exists() for x in args.input):
print("Must specify an existing file as input for the analyzer.")
return ERROR_NO_FILE
if args.output.exists() and not (args.overwrite or args.append):
print("Output file exists, pass --overwrite to overwrite or --append to add to exsiting analysis.")
return ERROR_EXISTING_OUTPUT
analyzer: AbstractAnalyzer
if args.mode == AnalyzeMode.english:
analyzer = English(args.window)
# we just dump a whole file into the english analyzer
for inp in args.input:
if not inp.exists():
print(f"warning {inp} does not exist")
continue
print(f"analyze: {inp}")
analyzer.analyze(inp.read_text('utf-8'))
else:
analyzer = Words(args.window)
# we do line-by-line single word dumps into the word analyzer
for inp in args.input:
if not inp.exists():
print(f"warning {inp} does not exist")
continue
print(f"analyze: {inp}")
with inp.open('r') as words:
for line in words:
analyzer.analyze(line.strip().lower())
if args.append:
# in append mode we load an existing chain file, and then run the analyzer and merge the contents.
with open(args.output, 'rb') as old:
chainer = load_chainer(old)
if chainer.order != analyzer.order:
print("Append chainer order isn't the same as the analyzer order.")
return ERROR_WRONG_ORDER
if chainer.analyzer_class != analyzer.__class__.__name__:
print("Append chainer class isn't the same as analyzer class.")
return ERROR_WRONG_CLASS
chainer.integrate(analyzer.chain_counts)
else:
chainer = from_analyzer(analyzer)
with open(args.output, 'wb') as output:
dump_chainer(chainer, output)
print(f"Wrote chainer to {args.output}")
if args.test:
r = Random()
for _ in range(0, 5):
print_chainer_output(chainer, r)
return 0
def command_chain(args: argparse.Namespace) -> int:
r = Random()
if not args.input.exists():
print("Must specify a chain file to load.")
return ERROR_NO_FILE
with args.input.open('rb') as inp:
chainer = load_chainer(inp)
if args.count < 1:
args.count = 1
for _ in range(0, args.count):
print_chainer_output(chainer, r, not args.unweighted)
return 0
def main() -> int:
args = parse_arguments()
if args.command == 'analyze':
return command_analyze(args)
elif args.command == 'chain':
return command_chain(args)
else:
print("Expect a command `analyze` or `chain`. See --help for details.")
return 1
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception:
print("Unexpected exception!")
traceback.print_exc()