Last active
September 5, 2024 07:36
-
-
Save kohya-ss/68d41a9720bfbdfd87869ec970142f4b to your computer and use it in GitHub Desktop.
ローカルLLMに小説を書いてもらう
This file contains 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
# Apache License 2.0 | |
# 使用法は gist のコメントを見てください | |
import time | |
import argparse | |
import os | |
import json | |
import tomli | |
import traceback | |
from typing import Any, Dict, List, Optional, Union, Iterator | |
from llama_cpp.llama_chat_format import _convert_completion_to_chat, register_chat_completion_handler | |
import llama_cpp.llama_types as llama_types | |
from llama_cpp.llama import LogitsProcessorList, LlamaGrammar | |
from llama_cpp import Llama, llama_chat_format | |
import llama_cpp | |
DEBUG_FLAG = False | |
# global option | |
FORCE_REVIEW_NG_1ST = False | |
DISABLE_SAVE_STATE = False | |
# global vars | |
FORCE_REVIEW_NG_STRING = "\n(編集長からのアドバイス:今回は初回のレビューなのでNGを出しましょう!)" | |
novelist_state = None | |
editor_state = None | |
number_of_reviews_per_generation = None | |
class GenerationParam: | |
def __init__( | |
self, | |
max_tokens=512, | |
temperature=0.2, | |
top_p=0.95, | |
top_k=40, | |
min_p=0.05, | |
typical_p=1.0, | |
repeat_penalty=1.1, | |
frequency_penalty=0.0, | |
presence_penalty=0.0, | |
): | |
self.max_tokens = max_tokens | |
self.temperature = temperature | |
self.top_p = top_p | |
self.top_k = top_k | |
self.min_p = min_p | |
self.typical_p = typical_p | |
self.repeat_penalty = repeat_penalty | |
self.frequency_penalty = frequency_penalty | |
self.presence_penalty = presence_penalty | |
# same as qwen 1.5 | |
@register_chat_completion_handler("qwen2") | |
def qwen2_chat_handler( | |
llama: Llama, | |
messages: List[llama_types.ChatCompletionRequestMessage], | |
functions: Optional[List[llama_types.ChatCompletionFunction]] = None, | |
function_call: Optional[llama_types.ChatCompletionRequestFunctionCall] = None, | |
tools: Optional[List[llama_types.ChatCompletionTool]] = None, | |
tool_choice: Optional[llama_types.ChatCompletionToolChoiceOption] = None, | |
temperature: float = 0.2, | |
top_p: float = 0.95, | |
top_k: int = 40, | |
min_p: float = 0.05, | |
typical_p: float = 1.0, | |
stream: bool = False, | |
stop: Optional[Union[str, List[str]]] = [], | |
response_format: Optional[llama_types.ChatCompletionRequestResponseFormat] = None, | |
max_tokens: Optional[int] = None, | |
presence_penalty: float = 0.0, | |
frequency_penalty: float = 0.0, | |
repeat_penalty: float = 1.1, | |
tfs_z: float = 1.0, | |
mirostat_mode: int = 0, | |
mirostat_tau: float = 5.0, | |
mirostat_eta: float = 0.1, | |
model: Optional[str] = None, | |
logits_processor: Optional[LogitsProcessorList] = None, | |
grammar: Optional[LlamaGrammar] = None, | |
assistant_gen_prefix: Optional[str] = None, | |
**kwargs, # type: ignore | |
) -> Union[llama_types.ChatCompletion, Iterator[llama_types.ChatCompletionChunk]]: | |
# Qwen1.5 | |
# '<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\nAIについて教えて<|im_end|>\n<|im_start|>assistant\n' | |
# Qwen2 | |
# "chat_template": "{% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n' }}{% endif %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}", | |
im_start = "<|im_start|>" | |
im_end = "<|im_end|>" | |
end_of_text = "<|endoftext|>" | |
# prompt = bos_token + start_turn_token | |
prompt = "" | |
if len(messages) > 0 and messages[0]["role"] == "system": | |
prompt += im_start + "system\n" + messages[0]["content"].strip() + im_end | |
messages = messages[1:] | |
for message in messages: | |
prompt += im_start + message["role"] + "\n" + message["content"] + im_end | |
prompt += im_start + "assistant\n" | |
if assistant_gen_prefix is not None: | |
prompt += assistant_gen_prefix | |
stop_tokens = [im_end, end_of_text] | |
return _convert_completion_to_chat( | |
llama.create_completion( | |
prompt=prompt, | |
temperature=temperature, | |
top_p=top_p, | |
top_k=top_k, | |
min_p=min_p, | |
typical_p=typical_p, | |
stream=stream, | |
stop=stop_tokens, | |
max_tokens=max_tokens, | |
presence_penalty=presence_penalty, | |
frequency_penalty=frequency_penalty, | |
repeat_penalty=repeat_penalty, | |
tfs_z=tfs_z, | |
mirostat_mode=mirostat_mode, | |
mirostat_tau=mirostat_tau, | |
mirostat_eta=mirostat_eta, | |
model=model, | |
logits_processor=logits_processor, | |
grammar=grammar, | |
), | |
stream=stream, | |
) | |
# the latest llama.cpp seems to have "command-r" handler, but we keep this until llama-cpp-python is updated | |
# we can also use the chat template from GGUF | |
@register_chat_completion_handler("command-r") | |
def command_r_chat_handler( | |
llama: Llama, | |
messages: List[llama_types.ChatCompletionRequestMessage], | |
functions: Optional[List[llama_types.ChatCompletionFunction]] = None, | |
function_call: Optional[llama_types.ChatCompletionRequestFunctionCall] = None, | |
tools: Optional[List[llama_types.ChatCompletionTool]] = None, | |
tool_choice: Optional[llama_types.ChatCompletionToolChoiceOption] = None, | |
temperature: float = 0.2, | |
top_p: float = 0.95, | |
top_k: int = 40, | |
min_p: float = 0.05, | |
typical_p: float = 1.0, | |
stream: bool = False, | |
stop: Optional[Union[str, List[str]]] = [], | |
response_format: Optional[llama_types.ChatCompletionRequestResponseFormat] = None, | |
max_tokens: Optional[int] = None, | |
presence_penalty: float = 0.0, | |
frequency_penalty: float = 0.0, | |
repeat_penalty: float = 1.1, | |
tfs_z: float = 1.0, | |
mirostat_mode: int = 0, | |
mirostat_tau: float = 5.0, | |
mirostat_eta: float = 0.1, | |
model: Optional[str] = None, | |
logits_processor: Optional[LogitsProcessorList] = None, | |
grammar: Optional[LlamaGrammar] = None, | |
assistant_gen_prefix: Optional[str] = None, | |
**kwargs, # type: ignore | |
) -> Union[llama_types.ChatCompletion, Iterator[llama_types.ChatCompletionChunk]]: | |
# bos_token = "<BOS_TOKEN>" | |
start_turn_token = "<|START_OF_TURN_TOKEN|>" | |
end_turn_token = "<|END_OF_TURN_TOKEN|>" | |
user_token = "<|USER_TOKEN|>" | |
chatbot_token = "<|CHATBOT_TOKEN|>" | |
system_token = "<|SYSTEM_TOKEN|>" | |
prompt = "" # bos_token # suppress warning | |
if len(messages) > 0 and messages[0]["role"] == "system": | |
prompt += start_turn_token + system_token + messages[0]["content"] + end_turn_token | |
messages = messages[1:] | |
for message in messages: | |
if message["role"] == "user": | |
prompt += start_turn_token + user_token + message["content"] + end_turn_token | |
elif message["role"] == "assistant": | |
prompt += start_turn_token + chatbot_token + message["content"] + end_turn_token | |
prompt += start_turn_token + chatbot_token | |
if assistant_gen_prefix is not None: | |
prompt += assistant_gen_prefix | |
if DEBUG_FLAG: | |
print(f"Prompt: {prompt}") | |
stop_tokens = [end_turn_token] # , bos_token] | |
return _convert_completion_to_chat( | |
llama.create_completion( | |
prompt=prompt, | |
temperature=temperature, | |
top_p=top_p, | |
top_k=top_k, | |
min_p=min_p, | |
typical_p=typical_p, | |
stream=stream, | |
stop=stop_tokens, | |
max_tokens=max_tokens, | |
presence_penalty=presence_penalty, | |
frequency_penalty=frequency_penalty, | |
repeat_penalty=repeat_penalty, | |
tfs_z=tfs_z, | |
mirostat_mode=mirostat_mode, | |
mirostat_tau=mirostat_tau, | |
mirostat_eta=mirostat_eta, | |
model=model, | |
logits_processor=logits_processor, | |
grammar=grammar, | |
# logprobs=4, | |
), | |
stream=stream, | |
) | |
@register_chat_completion_handler("gemma-2") | |
def gemma_2_chat_handler( | |
llama: Llama, | |
messages: List[llama_types.ChatCompletionRequestMessage], | |
functions: Optional[List[llama_types.ChatCompletionFunction]] = None, | |
function_call: Optional[llama_types.ChatCompletionRequestFunctionCall] = None, | |
tools: Optional[List[llama_types.ChatCompletionTool]] = None, | |
tool_choice: Optional[llama_types.ChatCompletionToolChoiceOption] = None, | |
temperature: float = 0.2, | |
top_p: float = 0.95, | |
top_k: int = 40, | |
min_p: float = 0.05, | |
typical_p: float = 1.0, | |
stream: bool = False, | |
stop: Optional[Union[str, List[str]]] = [], | |
response_format: Optional[llama_types.ChatCompletionRequestResponseFormat] = None, | |
max_tokens: Optional[int] = None, | |
presence_penalty: float = 0.0, | |
frequency_penalty: float = 0.0, | |
repeat_penalty: float = 1.1, | |
tfs_z: float = 1.0, | |
mirostat_mode: int = 0, | |
mirostat_tau: float = 5.0, | |
mirostat_eta: float = 0.1, | |
model: Optional[str] = None, | |
logits_processor: Optional[LogitsProcessorList] = None, | |
grammar: Optional[LlamaGrammar] = None, | |
assistant_gen_prefix: Optional[str] = None, | |
**kwargs, # type: ignore | |
) -> Union[llama_types.ChatCompletion, Iterator[llama_types.ChatCompletionChunk]]: | |
start_turn_token = "<start_of_turn>" | |
end_turn_token = "<end_of_turn>" | |
# system_str = "system\n" # Gemma-2、system promptに対応してないからどうしよう | |
system_str = "user\n" # user にしとこ | |
user_str = "user\n" | |
chatbot_str = "model\n" | |
prompt = "" # bos_token # suppress warning | |
if len(messages) > 0 and messages[0]["role"] == "system": | |
prompt += start_turn_token + system_str + messages[0]["content"] + end_turn_token | |
messages = messages[1:] | |
for message in messages: | |
if message["role"] == "user": | |
prompt += start_turn_token + user_str + message["content"] + end_turn_token | |
elif message["role"] == "assistant": | |
prompt += start_turn_token + chatbot_str + message["content"] + end_turn_token | |
prompt += start_turn_token + chatbot_str | |
if assistant_gen_prefix is not None: | |
prompt += assistant_gen_prefix | |
if DEBUG_FLAG: | |
print(f"Prompt: {prompt}") | |
stop_tokens = [end_turn_token] # , bos_token] | |
return _convert_completion_to_chat( | |
llama.create_completion( | |
prompt=prompt, | |
temperature=temperature, | |
top_p=top_p, | |
top_k=top_k, | |
min_p=min_p, | |
typical_p=typical_p, | |
stream=stream, | |
stop=stop_tokens, | |
max_tokens=max_tokens, | |
presence_penalty=presence_penalty, | |
frequency_penalty=frequency_penalty, | |
repeat_penalty=repeat_penalty, | |
tfs_z=tfs_z, | |
mirostat_mode=mirostat_mode, | |
mirostat_tau=mirostat_tau, | |
mirostat_eta=mirostat_eta, | |
model=model, | |
logits_processor=logits_processor, | |
grammar=grammar, | |
# logprobs=4, | |
), | |
stream=stream, | |
) | |
def get_chat_completion_handler(chat_handler_name): | |
if chat_handler_name == "command-r" or chat_handler_name == "qwen2" or chat_handler_name == "gemma-2": | |
return llama_chat_format.get_chat_completion_handler(chat_handler_name) # return custom chat handler | |
# copy from llama_chat_format.py | |
# build chat formatter -> override it -> build chat completion handler -> return it | |
chat_formatter = None | |
if chat_handler_name == "vicuna": | |
# chat_formatter = llama_chat_format.format | |
# vicuna の formatter は system prompt を反映しないので自前で実装 | |
from llama_cpp.llama_chat_format import _format_add_colon_two, _map_roles, ChatFormatterResponse | |
def format(messages: List[llama_types.ChatCompletionRequestMessage], **kwargs: Any) -> ChatFormatterResponse: | |
_system_message = "" | |
for message in messages: | |
if message["role"] == "system": | |
_system_message = message["content"] | |
break | |
_roles = dict(user="USER", assistant="ASSISTANT") | |
_sep = " " | |
_sep2 = "</s>" | |
_messages = _map_roles(messages, _roles) | |
_messages.append((_roles["assistant"], None)) | |
_prompt = _format_add_colon_two(_system_message, _messages, _sep, _sep2) | |
return ChatFormatterResponse(prompt=_prompt) | |
chat_formatter = format | |
else: | |
for formatter_name in [chat_handler_name, chat_handler_name.replace("-", "_"), chat_handler_name.replace("-", "")]: | |
try: | |
chat_formatter = getattr(llama_chat_format, f"format_{formatter_name}") | |
break | |
except AttributeError: | |
pass | |
if chat_formatter is None: | |
raise ValueError(f"Invalid chat handler: {chat_handler_name}") | |
original_chat_formatter = chat_formatter | |
# override the formatter | |
def formatter_wrapper(messages, **kwargs): | |
# messages is modified to (messages, prefix) | |
messages, assistant_gen_prefix = messages | |
response = original_chat_formatter(messages, **kwargs) | |
prompt = response.prompt | |
# print(f"formatter_wrapper is called. prompt: {prompt}, assistant_gen_prefix: {assistant_gen_prefix}") | |
if prompt and assistant_gen_prefix: | |
prompt += assistant_gen_prefix | |
response.prompt = prompt | |
return response | |
# build chat completion handler | |
original_handler = llama_chat_format.chat_formatter_to_chat_completion_handler(formatter_wrapper) | |
def handler_wrapper(*args, **kwargs): | |
messages = kwargs.get("messages", None) | |
if messages: | |
messages = (messages, kwargs.get("assistant_gen_prefix", "")) | |
kwargs["messages"] = messages | |
return original_handler(*args, **kwargs) | |
# print(f"Chat handler is wrapped: {chat_handler_name}") | |
return handler_wrapper | |
class ModelWrapper: | |
def __init__(self): | |
pass | |
def generate(self, messages: List[Dict[str, str]], generation_param: GenerationParam) -> str: | |
raise NotImplementedError() | |
def load_state(self, state): | |
return None | |
def save_state(self): | |
pass | |
class LlamaModelWrapper(ModelWrapper): | |
def __init__(self, llama, handler): | |
self.llama = llama | |
self.handler = handler | |
def load_state(self, state): | |
print("Loading state") | |
self.llama.load_state(state) | |
def save_state(self): | |
print("Saving state") | |
return self.llama.save_state() | |
def generate(self, messages: List[Dict[str, str]], generation_param: GenerationParam) -> str: | |
response = self.handler( | |
llama=self.llama, | |
messages=messages, | |
max_tokens=generation_param.max_tokens, | |
temperature=generation_param.temperature, | |
top_p=generation_param.top_p, | |
repeat_penalty=generation_param.repeat_penalty, | |
top_k=int(generation_param.top_k), | |
min_p=generation_param.min_p, | |
typical_p=generation_param.typical_p, | |
stream=False, | |
) | |
content = response["choices"][0]["message"]["content"] | |
return content | |
class TransformersModelWrapper(ModelWrapper): | |
def __init__(self, model_id: str, tensor_split: Optional[str]): | |
from transformers import AutoModelForCausalLM, AutoTokenizer | |
if tensor_split is not None: | |
# tensor split to device_map | |
max_memory = {} | |
for i, memory in enumerate(tensor_split.split(",")): | |
max_memory[i] = f"{memory}GB" | |
else: | |
max_memory = None | |
self.model = AutoModelForCausalLM.from_pretrained(args.model, device_map="auto", torch_dtype="auto", max_memory=max_memory) | |
self.tokenizer = AutoTokenizer.from_pretrained(args.model) | |
self.is_gemma_2 = "gemma-2" in model_id.lower() | |
def generate(self, messages: List[Dict[str, str]], generation_param: GenerationParam) -> str: | |
if self.is_gemma_2: | |
# gemma 2 does not use system prompt, so we concat them | |
if len(messages) > 1 and messages[0]["role"] == "system" and messages[1]["role"] == "user": | |
new_msgs = [] | |
new_msgs.append({"role": "user", "content": messages[0]["content"] + "\n\n" + messages[1]["content"]}) | |
new_msgs.extend(messages[2:]) | |
messages = new_msgs | |
if DEBUG_FLAG: | |
print(f"Messages: {messages}") | |
token_ids = self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt") | |
token_ids = token_ids.to("cuda") | |
do_sample_flag = True | |
output_ids = self.model.generate( | |
token_ids.to(self.model.device), | |
temperature=generation_param.temperature, | |
do_sample=do_sample_flag, | |
top_p=generation_param.top_p, | |
top_k=generation_param.top_k, | |
max_new_tokens=generation_param.max_tokens, | |
repetition_penalty=generation_param.repeat_penalty, | |
) | |
input_len = len(token_ids[0]) | |
output = self.tokenizer.decode(output_ids[0][input_len:], skip_special_tokens=True) | |
if DEBUG_FLAG: | |
print(f"Output: {output}") | |
return output | |
def log(log_history, msg): | |
timestamp_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) | |
log_history.append(f"{timestamp_str}: {msg}") | |
print(msg) | |
def build_message_simple(system, user): | |
messages = [] | |
if system: | |
messages.append({"role": "system", "content": system}) | |
if user: | |
messages.append({"role": "user", "content": user}) | |
return messages | |
def build_messages(system, user_assistant_pairs): | |
messages = [] | |
if system: | |
messages.append({"role": "system", "content": system}) | |
for user, assistant in user_assistant_pairs: | |
if user: | |
messages.append({"role": "user", "content": user}) | |
if assistant: | |
messages.append({"role": "assistant", "content": assistant}) | |
return messages | |
def generate_response(is_novelist, is_editor, model: ModelWrapper, generation_param, messages): | |
global DISABLE_SAVE_STATE | |
global novelist_state | |
global editor_state | |
if not DISABLE_SAVE_STATE: | |
if is_novelist: | |
if novelist_state is not None: | |
model.load_state(novelist_state) | |
if is_editor: | |
if editor_state is not None: | |
model.load_state(editor_state) | |
response = model.generate(messages, generation_param) | |
# if DEBUG_FLAG: | |
# print(f"Messsages: {messages}") | |
# if DEBUG_FLAG: | |
# print(f"Response: {content}") | |
content = response | |
if not DISABLE_SAVE_STATE: | |
if is_novelist: | |
novelist_state = model.save_state() | |
if is_editor: | |
editor_state = model.save_state() | |
return content | |
def check_review_response_ok_ng(response: str, retry: int): | |
# LLMに判断させた方がいい気がする。「OK目指して頑張りましょう」とか言われたらヤバい | |
# NGがあり、OKがない場合はNG | |
ng_p = response.lower().find("ng") | |
ok_p = response.lower().find("ok") | |
if ng_p >= 0 and ok_p < 0: | |
return False, response | |
# check the last position of "OK" and "NG", and use the latest one | |
ok_p = response.lower().rfind("ok") | |
ng_p = response.lower().rfind("ng") | |
review_ok = ok_p >= 0 and (ng_p < 0 or ok_p > ng_p) | |
return review_ok, response | |
def generate_plot(model, prompt_data, retry_limit=3, retry_limit_per_plot=3): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["plot_parameters"]) | |
if DEBUG_FLAG: | |
print(f"Plot generation param: {prompt_data['plot_parameters']}") | |
novelist_prompts = prompt_data["plot_novelist"] | |
editor_prompts = prompt_data["plot_editor"] | |
review_ok = False | |
for retry in range(retry_limit): | |
novelist_pairs = [] | |
# 何から始める? → こういうプロットを書いて | |
editor_pairs = [(editor_prompts["user_prompt_make"], novelist_prompts["user_prompt_make"])] | |
review_comments = [] | |
plots = [] | |
for review_idx in range(retry_limit_per_plot): | |
# 1. make a plot with the novelist agent | |
system = novelist_prompts["system_prompt"] | |
if len(plots) == 0: | |
novelist_pairs.append((novelist_prompts["user_prompt_make"], None)) | |
else: | |
novelist_pairs.append((novelist_prompts["user_prompt_revise"].replace("$comment", review_comments[-1]), None)) | |
log(logs, f"Retry: {retry}-{review_idx}, Novelist request: {novelist_pairs[-1][0]}") | |
messages = build_messages(system, novelist_pairs) | |
response = generate_response(True, False, model, generation_param, messages) | |
# remove newline characters | |
response = response.replace("\n", "") | |
log(logs, f"Retry: {retry}-{review_idx}, Novelist 1st response: {response}") | |
novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
plots.append(response) | |
if number_of_reviews_per_generation is not None and review_idx >= number_of_reviews_per_generation: | |
review_ok = True | |
break | |
# 2. review the plot with the editor agent | |
system = editor_prompts["system_prompt"] | |
if review_idx == 0: | |
editor_pairs.append((editor_prompts["user_prompt_review"].replace("$plot", plots[-1]), None)) | |
else: | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$plot", plots[-1]), None)) | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0] + FORCE_REVIEW_NG_STRING, None) | |
log(logs, f"Retry: {retry}-{review_idx}, Editor request: {editor_pairs[-1][0]}") | |
messages = build_messages(system, editor_pairs) | |
response = generate_response(False, True, model, generation_param, messages) | |
log(logs, f"Review {retry}-{review_idx}, Editor response: {response}") | |
editor_pairs[-1] = (editor_pairs[-1][0], response) | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0][: -len(FORCE_REVIEW_NG_STRING)], response) | |
review_ok, body = check_review_response_ok_ng(response, review_idx) | |
if review_ok: | |
break | |
review_comments.append(body) | |
if review_ok: | |
break | |
if not review_ok: | |
print("Failed to generate plot") | |
return None, logs | |
return plots[-1], logs | |
def generate_characters_rough(model, prompt_data, plot, retry_limit=3, retry_limit_per_characters=3): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["character_rough_parameters"]) | |
if DEBUG_FLAG: | |
print(f"Character rough generation param: {prompt_data['character_rough_parameters']}") | |
novelist_prompts = prompt_data["character_rough_novelist"] | |
editor_prompts = prompt_data["character_rough_editor"] | |
review_ok = False | |
for retry in range(retry_limit): | |
# 1. talk about characters with the novelist agent | |
# この部分まで含めて review した方がいいかもしれない | |
system = novelist_prompts["system_prompt"] | |
novelist_user_prompt_1st = novelist_prompts["user_prompt_make_rough1"].replace("$plot", plot) | |
user = novelist_user_prompt_1st | |
log(logs, f"Retry: {retry}, Novelist 1st request: {user}") | |
messages = build_message_simple(system, user) | |
response = generate_response(True, False, model, generation_param, messages) | |
characters_free_description = response.strip() | |
log(logs, f"Retry: {retry}, Novelist 1st response: {response}") | |
# 2. make them more detailed with the novelist agent | |
novelist_pairs = [(novelist_user_prompt_1st, characters_free_description)] | |
editor_pairs = [] | |
review_comments = [] | |
characters_ideas = [] | |
for review_idx in range(retry_limit_per_characters): | |
system = novelist_prompts["system_prompt"] | |
if len(characters_ideas) == 0: | |
novelist_pairs.append((novelist_prompts["user_prompt_make_rough2"], None)) | |
else: | |
novelist_pairs.append((novelist_prompts["user_prompt_revise_rough"].replace("$comment", review_comments[-1]), None)) | |
log(logs, f"Retry: {retry}-{review_idx}, Novelist request: {novelist_pairs[-1][0]}") | |
messages = build_messages(system, novelist_pairs) | |
response = generate_response(True, False, model, generation_param, messages) | |
log(logs, f"Retry: {retry}-{review_idx}, Novelist response: {response}") | |
novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
characters_idea = response.strip() | |
characters_ideas.append(characters_idea) | |
if number_of_reviews_per_generation is not None and review_idx >= number_of_reviews_per_generation: | |
review_ok = True | |
break | |
# 3. review the characters with the editor agent | |
system = editor_prompts["system_prompt"] | |
if len(characters_ideas) == 1: | |
editor_pairs.append( | |
( | |
editor_prompts["user_prompt_review_rough"].replace("$plot", plot).replace("$characters", characters_idea), | |
None, | |
) | |
) | |
else: | |
editor_pairs.append( | |
( | |
editor_prompts["user_prompt_revise_rough"].replace("$characters", characters_idea), | |
None, | |
) | |
) | |
# ここではFORCE_REVIEW_NG_1STが指定されていてもNGを出さない:登場人物が増えたりするので | |
# if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
# editor_pairs[-1] = (editor_pairs[-1][0] + FORCE_REVIEW_NG_STRING, None) | |
log(logs, f"Retry: {retry}-{review_idx}, Editor request: {editor_pairs[-1][0]}") | |
messages = build_messages(system, editor_pairs) | |
response = generate_response(False, True, model, generation_param, messages) | |
log(logs, f"Review {retry}-{review_idx}, Editor response: {response}") | |
editor_pairs[-1] = (editor_pairs[-1][0], response) | |
# if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
# editor_pairs[-1] = (editor_pairs[-1][0][: -len(FORCE_REVIEW_NG_STRING)], response) | |
review_ok, body = check_review_response_ok_ng(response, review_idx) | |
if review_ok: | |
break | |
review_comments.append(body) | |
# novelist_pairs.append((novelist_prompts["user_prompt_make_rough2"], None)) | |
# messages = build_messages(system, novelist_pairs) | |
# response = generate_response(True, False, model, generation_param, messages) | |
# log(logs, f"Retry: {retry}, Novelist 2nd response: {response}") | |
# novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
# review_comments = [] | |
# characters_ideas = [response] | |
# for review_idx in range(retry_limit_per_characters): | |
# # 3. review the characters with the editor agent | |
# system = editor_prompts["system_prompt"] | |
# if len(characters_ideas) == 1: | |
# pairs = [ | |
# ( | |
# editor_prompts["user_prompt_review_rough"] | |
# .replace("$plot", plot) | |
# .replace("$characters", characters_ideas[0]), | |
# None, | |
# ) | |
# ] | |
# else: | |
# pairs = [ | |
# ( | |
# editor_prompts["user_prompt_review_rough"] | |
# .replace("$plot", plot) | |
# .replace("$characters", characters_ideas[0]), | |
# review_comments[0], | |
# ) | |
# ] | |
# for character_idea, comment in zip(characters_ideas[1:-1], review_comments[1:]): | |
# pairs.append((editor_prompts["user_prompt_revise_rough"].replace("$characters", character_idea), comment)) | |
# pairs.append((editor_prompts["user_prompt_revise_rough"].replace("$characters", characters_ideas[-1]), None)) | |
# messages = build_messages(system, pairs) | |
# response = generate_response(False, True, model, generation_param, messages) | |
# log(logs, f"Review {review_idx}, Editor response: {response}") | |
# if "step 1" not in response.lower(): | |
# print(f"Warning: Invalid editor first response: {response}") | |
# # break | |
# review_ok, body = check_step_1_2_response(response, review_idx) | |
# if review_ok: | |
# break | |
# review_comments.append(body) | |
# # call novelist again | |
# system = novelist_prompts["system_prompt"] | |
# novelist_pairs.append((novelist_prompts["user_prompt_revise_rough"].replace("$comment", review_comments[-1]), None)) | |
# messages = build_messages(system, pairs) | |
# response = generate_response(True, False, model, generation_param, messages) | |
# novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
# log(logs, f"Review {review_idx}, Novelist response: {response}") | |
# characters_ideas.append(response) | |
if review_ok: | |
break | |
if not review_ok: | |
print("Failed to generate characters") | |
return None, logs | |
return characters_ideas[-1], logs | |
def generate_cleaned_characters_rough(model, prompt_data, characters_rough): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["character_rough_parameters"]) | |
refiner_prompts = prompt_data["character_rough_refiner"] | |
system = refiner_prompts["system_prompt"] | |
# refine 1st stage: remove multiple chars etc. | |
user = refiner_prompts["user_prompt_clean_up"].replace("$characters", characters_rough) | |
messages = build_message_simple(system, user) | |
response = generate_response(False, False, model, generation_param, messages) | |
characters_rough = response.strip() | |
log(logs, f"Refiner 1st response: {response}") | |
# 2nd stage: remove comment from first/last lines | |
user_is_comment = refiner_prompts["user_prompt_is_comment"] | |
flag_text = refiner_prompts["flag_text"] | |
# split to lines and remove emtpy lines | |
lines = [line.strip() for line in characters_rough.split("\n") if line.strip()] | |
# check if the first and last lines are comments | |
for i, line in enumerate([lines[0], lines[-1]]): | |
user = user_is_comment.replace("$line", line) | |
messages = build_message_simple(system, user) | |
response = generate_response(False, False, model, generation_param, messages) | |
log(logs, f"Scenes refiner {i} response: {response}") | |
if flag_text in response: | |
if i == 0: | |
lines = lines[1:] | |
else: | |
lines = lines[:-1] | |
# join to a single string | |
characters_rough = "\n".join(lines) | |
return characters_rough, logs | |
def generate_characters_detailed( | |
model, | |
prompt_data, | |
plot, | |
char_index, | |
rough_characters, | |
detailed_characters, | |
retry_limit=3, | |
retry_limit_per_characters=3, | |
): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["character_detail_parameters"]) | |
if DEBUG_FLAG: | |
print(f"Character detail generation param: {prompt_data['character_detail_parameters']}") | |
novelist_prompts = prompt_data["character_detail_novelist"] | |
editor_prompts = prompt_data["character_detail_editor"] | |
rough_characters_str = "\n".join([f"{i+1}. {c}" for i, c in enumerate(rough_characters)]) | |
# loop for each character | |
review_ok = False | |
character_detail = None # final character detail | |
for retry in range(retry_limit): | |
novelist_pairs = [] | |
editor_pairs = [] | |
review_comments = [] | |
character_detail_candidates = [] | |
for review_idx in range(retry_limit_per_characters): | |
# 1. make more detailed characters with the novelist agent | |
system = novelist_prompts["system_prompt"] | |
if len(review_comments) == 0: | |
user = ( | |
novelist_prompts["user_prompt_make1"].replace("$plot", plot).replace("$rough_characters", rough_characters_str) | |
) | |
if char_index > 0: | |
detailed_characters_str = "\n".join([f"{i+1}. {c}\n\n" for i, c in enumerate(detailed_characters)]) | |
user += novelist_prompts["user_prompt_make2"].replace("$characters", detailed_characters_str) | |
user += novelist_prompts["user_prompt_make3"].replace("$character", rough_characters[char_index]) | |
user += novelist_prompts["user_prompt_make_revise_common"] | |
else: | |
user = novelist_prompts["user_prompt_revise"].replace("$comment", review_comments[-1]) | |
user += novelist_prompts["user_prompt_make_revise_common"] | |
novelist_pairs.append((user, None)) | |
log(logs, f"Character {char_index}, Retry {retry}-{review_idx}, Novelist request: {novelist_pairs[-1][0]}") | |
messages = build_messages(system, novelist_pairs) | |
response = generate_response(True, False, model, generation_param, messages) | |
log(logs, f"Character {char_index}, Retry {retry}-{review_idx}, Novelist response: {response}") | |
novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
character_detail = response.strip() | |
character_detail_candidates.append(response) | |
if number_of_reviews_per_generation is not None and review_idx >= number_of_reviews_per_generation: | |
review_ok = True | |
break | |
# 2. review the characters with the editor agent | |
system = editor_prompts["system_prompt"] | |
if len(character_detail_candidates) == 1: | |
user = ( | |
editor_prompts["user_prompt_review1"].replace("$plot", plot).replace("$rough_characters", rough_characters_str) | |
) | |
if char_index > 0: | |
detailed_characters_str = "\n".join([f"{i+1}. {c}" for i, c in enumerate(detailed_characters)]) | |
user += editor_prompts["user_prompt_review2"].replace("$characters", detailed_characters_str) | |
user += ( | |
editor_prompts["user_prompt_review3"] | |
.replace("$character_detail", character_detail) | |
.replace("$character", rough_characters[char_index]) | |
) | |
else: | |
user = editor_prompts["user_prompt_revise"].replace("$character_detail", character_detail) | |
editor_pairs.append((user, None)) | |
# ここではFORCE_REVIEW_NG_1STが指定されていてもNGを出さない:そこまで大きく揺れないので | |
# if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
# editor_pairs[-1] = (editor_pairs[-1][0] + FORCE_REVIEW_NG_STRING, None) | |
log(logs, f"Character {char_index}, Retry {retry}-{review_idx}, Editor request: {editor_pairs[-1][0]}") | |
messages = build_messages(system, editor_pairs) | |
response = generate_response(False, True, model, generation_param, messages) | |
log(logs, f"Character {char_index}, Retry {retry}-{review_idx}, Editor response: {response}") | |
# if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
# editor_pairs[-1] = (editor_pairs[-1][0][: -len(FORCE_REVIEW_NG_STRING)], response) | |
review_ok, body = check_review_response_ok_ng(response, review_idx) | |
if review_ok: | |
break | |
review_comments.append(body) | |
if review_ok: | |
break | |
if not review_ok: | |
print(f"Failed to generate character {char_index}") | |
return False, None, logs # return the generated characters | |
return True, character_detail, logs | |
def generate_cleaned_character_detail(model, prompt_data, character_rough, character_detail): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["character_detail_parameters"]) | |
refiner_prompts = prompt_data["character_detail_refiner"] | |
system = refiner_prompts["system_prompt"] | |
# check if the first and last lines are comments | |
user_is_comment = refiner_prompts["user_prompt_is_comment"] | |
flag_text = refiner_prompts["flag_text"] | |
# split outline to lines and remove emtpy lines | |
lines = [line.strip() for line in [character_rough] + character_detail.split("\n") if line.strip()] | |
# check if the first and last lines are comments | |
for i, line in enumerate([lines[0], lines[-1]]): | |
user = user_is_comment.replace("$line", line) | |
messages = build_message_simple(system, user) | |
response = generate_response(False, False, model, generation_param, messages) | |
log(logs, f"Scenes refiner {i} response: {response}") | |
if flag_text in response: | |
if i == 0: | |
lines = lines[1:] | |
else: | |
lines = lines[:-1] | |
# duplicate check for rough and 1st line of detail | |
user_is_duplicate = refiner_prompts["user_prompt_is_duplicate"] | |
dup_flag_text = refiner_prompts["dup_flag_text"] | |
character_detail = character_detail.split("\n") | |
user_is_duplicate = user_is_duplicate.replace("$text1", lines[0]).replace("$text2", lines[1]) | |
messages = build_message_simple(system, user_is_duplicate) | |
response = generate_response(False, False, model, generation_param, messages) | |
log(logs, f"Character refiner duplicate check response: {response}") | |
if dup_flag_text in response: | |
lines = lines[1:] | |
# join to a single string | |
character_detail = "\n".join(lines) | |
return character_detail, logs | |
# user = ( | |
# refiner_prompts["user_prompt_clean_up"] | |
# .replace("$character_rough", character_rough) | |
# .replace("$character_detail", character_detail) | |
# ) | |
# messages = build_message_simple(system, user) | |
# response = generate_response(model, generation_param, messages) | |
# cleaned = response | |
# log(logs, f"Refiner 1st response: {response}") | |
# return cleaned, logs | |
def generate_outline(model, prompt_data, plot, rough_characters, detailed_characters, retry_limit=3, retry_limit_per_outline=3): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["outline_parameters"]) | |
if DEBUG_FLAG: | |
print(f"Outline generation param: {prompt_data['outline_parameters']}") | |
novelist_prompts = prompt_data["outline_novelist"] | |
editor_prompts = prompt_data["outline_editor"] | |
rough_characters_str = "\n".join([f"{i+1}. {c}" for i, c in enumerate(rough_characters)]) | |
detailed_characters_str = "\n\n".join([f"{i+1}. {c}" for i, c in enumerate(detailed_characters)]) | |
review_ok = False | |
for retry in range(retry_limit): | |
novelist_pairs = [] | |
editor_pairs = [] | |
outlines = [] | |
review_comments = [] | |
for review_idx in range(retry_limit_per_outline): | |
# 1. make an outline with the novelist agent | |
system = novelist_prompts["system_prompt"] | |
if len(outlines) == 0: | |
novelist_pairs.append( | |
( | |
novelist_prompts["user_prompt_make1"] | |
.replace("$plot", plot) | |
.replace("$rough_characters", rough_characters_str) | |
.replace("$detailed_characters", detailed_characters_str), | |
None, | |
) | |
) | |
else: | |
novelist_pairs.append((novelist_prompts["user_prompt_revise"].replace("$comment", review_comments[-1]), None)) | |
log(logs, f"Outline {retry}-{review_idx}, Novelist request: {novelist_pairs[-1][0]}") | |
messages = build_messages(system, novelist_pairs) | |
response = generate_response(True, False, model, generation_param, messages) | |
log(logs, f"Outline {retry}-{review_idx}, Novelist response: {response}") | |
novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
outlines.append(response) | |
if number_of_reviews_per_generation is not None and review_idx >= number_of_reviews_per_generation: | |
review_ok = True | |
break | |
# 2. review the outline with the editor agent | |
system = editor_prompts["system_prompt"] | |
if len(outlines) == 1: | |
user = ( | |
editor_prompts["user_prompt_review1"] | |
.replace("$plot", plot) | |
.replace("$rough_characters", rough_characters_str) | |
.replace("$detailed_characters", detailed_characters_str) | |
.replace("$outline", outlines[-1]) | |
) | |
editor_pairs.append((user, None)) | |
else: | |
for outline, comment in zip(outlines[:-1], review_comments): | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$outline", outline), comment)) | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$outline", outlines[-1]), None)) | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0] + FORCE_REVIEW_NG_STRING, None) | |
log(logs, f"Outline {retry}-{review_idx}, Editor request: {editor_pairs[-1][0]}") | |
messages = build_messages(system, editor_pairs) | |
response = generate_response(False, True, model, generation_param, messages) | |
log(logs, f"Review {retry}-{review_idx}, Editor response: {response}") | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0][: -len(FORCE_REVIEW_NG_STRING)], response) | |
review_ok, body = check_review_response_ok_ng(response, review_idx) | |
if review_ok: | |
break | |
review_comments.append(body) | |
if review_ok: | |
break | |
if not review_ok: | |
print("Failed to generate outline") | |
return None, logs | |
return outlines[-1], logs | |
def generate_cleaned_outline_and_split(model, prompt_data, outline, cleaning=True): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["outline_parameters"]) | |
refiner_prompts = prompt_data["outline_refiner"] | |
system = refiner_prompts["system_prompt"] | |
user_is_comment = refiner_prompts["user_prompt_is_comment"] | |
flag_text = refiner_prompts["flag_text"] | |
# split outline to lines and remove emtpy lines | |
lines = [line.strip() for line in outline.split("\n") if line.strip()] | |
# check if the first and last lines are comments | |
if cleaning: | |
for i, line in enumerate([lines[0], lines[-1]]): | |
user = user_is_comment.replace("$line", line) | |
messages = build_message_simple(system, user) | |
response = generate_response(False, False, model, generation_param, messages) | |
log(logs, f"Refiner {i} response: {response}") | |
if flag_text in response: | |
if i == 0: | |
lines = lines[1:] | |
else: | |
lines = lines[:-1] | |
# split to each sections | |
if "第" in outline and "章" in outline: # モデル出力に依存してるので変えたい | |
sections = [] | |
section = None | |
for line in lines: | |
if "第" in line and "章" in line: | |
if section: | |
sections.append(section) | |
section = [] | |
if section is not None: # ignore before first "第n章" | |
section.append(line) | |
if section: | |
sections.append(section) | |
# convert section to a single string | |
sections = ["\n".join(section) for section in sections] | |
else: | |
sections = lines | |
# remove empty sections | |
sections = [section for section in sections if section] | |
return sections, logs | |
# user = refiner_prompts["user_prompt_clean_up"].replace("$outline", outline) | |
# messages = build_message_simple(system, user) | |
# response = generate_response(model, generation_param, messages) | |
# cleaned = response | |
# log(logs, f"Refiner 1st response: {response}") | |
# return cleaned, logs | |
def generate_scenes( | |
model, | |
prompt_data, | |
section_index, | |
plot, | |
rough_characters, | |
detailed_characters, | |
outline, | |
retry_limit=3, | |
retry_limit_per_outline=3, | |
): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["scenes_parameters"]) | |
if DEBUG_FLAG: | |
print(f"Scenes generation param: {prompt_data['scenes_parameters']}") | |
novelist_prompts = prompt_data["scenes_novelist"] | |
editor_prompts = prompt_data["scenes_editor"] | |
rough_characters_str = "\n".join([f"{i+1}. {c}" for i, c in enumerate(rough_characters)]) | |
detailed_characters_str = "\n\n".join([f"{i+1}. {c}" for i, c in enumerate(detailed_characters)]) | |
outline_str = "\n\n".join(outline) | |
section_outline_str = outline[section_index] | |
section_number = "一二三四五六七八九十"[section_index] if section_index < 10 else f"{section_index+1}" | |
section_number = "第" + section_number + "章" | |
review_ok = False | |
for retry in range(retry_limit): | |
novelist_pairs = [] | |
editor_pairs = [] | |
scenes_list = [] | |
review_comments = [] | |
for review_idx in range(retry_limit_per_outline): | |
# 1. make an outline with the novelist agent | |
system = novelist_prompts["system_prompt"] | |
if len(scenes_list) == 0: | |
novelist_pairs.append( | |
( | |
novelist_prompts["user_prompt_make1"] | |
.replace("$plot", plot) | |
.replace("$rough_characters", rough_characters_str) | |
.replace("$detailed_characters", detailed_characters_str) | |
.replace("$outline_section", section_outline_str) | |
.replace("$outline", outline_str) | |
.replace("$section_number", section_number), | |
None, | |
) | |
) | |
else: | |
novelist_pairs.append((novelist_prompts["user_prompt_revise"].replace("$comment", review_comments[-1]), None)) | |
log(logs, f"Scenes {section_index}-{retry}-{review_idx}, Novelist request: {novelist_pairs[-1][0]}") | |
messages = build_messages(system, novelist_pairs) | |
response = generate_response(True, False, model, generation_param, messages) | |
log(logs, f"Scenes {section_index}-{retry}-{review_idx}, Novelist response: {response}") | |
novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
scenes_list.append(response) | |
if number_of_reviews_per_generation is not None and review_idx >= number_of_reviews_per_generation: | |
review_ok = True | |
break | |
# 2. review the scenes with the editor agent | |
system = editor_prompts["system_prompt"] | |
if len(scenes_list) == 1: | |
user = ( | |
editor_prompts["user_prompt_review1"] | |
.replace("$plot", plot) | |
.replace("$rough_characters", rough_characters_str) | |
.replace("$detailed_characters", detailed_characters_str) | |
.replace("$outline_section", section_outline_str) | |
.replace("$outline", outline_str) | |
.replace("$section_number", section_number) | |
.replace("$scenes", scenes_list[-1]) | |
) | |
editor_pairs.append((user, None)) | |
else: | |
for scenes, comment in zip(scenes_list[:-1], review_comments): | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$scenes", scenes), comment)) | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$scenes", scenes_list[-1]), None)) | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0] + FORCE_REVIEW_NG_STRING, None) | |
log(logs, f"Scenes {section_index}-{retry}-{review_idx}, Editor request: {editor_pairs[-1][0]}") | |
messages = build_messages(system, editor_pairs) | |
response = generate_response(False, True, model, generation_param, messages) | |
log(logs, f"Review {section_index}-{retry}-{review_idx}, Editor response: {response}") | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0][: -len(FORCE_REVIEW_NG_STRING)], response) | |
review_ok, body = check_review_response_ok_ng(response, review_idx) | |
if review_ok: | |
break | |
review_comments.append(body) | |
if review_ok: | |
break | |
if not review_ok: | |
print("Failed to generate scenes") | |
return None, logs | |
return scenes_list[-1], logs | |
def generate_cleaned_scenes(model, prompt_data, scenes): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["scenes_parameters"]) | |
refiner_prompts = prompt_data["scenes_refiner"] | |
system = refiner_prompts["system_prompt"] | |
user_is_comment = refiner_prompts["user_prompt_is_comment"] | |
flag_text = refiner_prompts["flag_text"] | |
# split outline to lines and remove emtpy lines | |
lines = [line.strip() for line in scenes.split("\n") if line.strip()] | |
# check if the first and last lines are comments | |
for i, line in enumerate([lines[0], lines[-1]]): | |
user = user_is_comment.replace("$line", line) | |
messages = build_message_simple(system, user) | |
response = generate_response(False, False, model, generation_param, messages) | |
log(logs, f"Scenes refiner {i} response: {response}") | |
if flag_text in response: | |
if i == 0: | |
lines = lines[1:] | |
else: | |
lines = lines[:-1] | |
# join to a single string | |
scenes = "\n".join(lines) | |
return scenes, logs | |
def generate_text( | |
model, | |
prompt_data, | |
section_index, | |
plot, | |
rough_characters, | |
detailed_characters, | |
outline, | |
section_scenes, | |
texts, | |
retry_limit=3, | |
retry_limit_per_text=3, | |
): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["text_parameters"]) | |
if DEBUG_FLAG: | |
print(f"Text generation param: {prompt_data['text_parameters']}") | |
def section_index_to_str(i): | |
s = "一二三四五六七八九十"[i] if i < 10 else f"{i+1}" | |
return "第" + s + "章" | |
novelist_prompts = prompt_data["text_novelist"] | |
editor_prompts = prompt_data["text_editor"] | |
rough_characters_str = "\n".join([f"{i+1}. {c}" for i, c in enumerate(rough_characters)]) | |
detailed_characters_str = "\n\n".join([f"{i+1}. {c}" for i, c in enumerate(detailed_characters)]) | |
outline_str = "\n\n".join(outline) | |
section_outline_str = outline[section_index] | |
section_number = section_index_to_str(section_index) | |
section_scenes_str = section_scenes[section_index] | |
num_lines_prev_section_text = novelist_prompts.get("num_lines_prev_section_text", 10) | |
fixed_review_comment = editor_prompts.get("fixed_review_comment", None) | |
if section_index > 0 and num_lines_prev_section_text > 0: | |
prev_section_number = section_index_to_str(section_index - 1) | |
prev_section_text = texts[section_index - 1] | |
lines = prev_section_text.split("。") | |
if len(lines) > num_lines_prev_section_text: | |
lines = lines[-num_lines_prev_section_text:] | |
prev_section_text = "。".join(lines) | |
else: | |
prev_section_number = "" | |
prev_section_text = "" | |
review_ok = False | |
for retry in range(retry_limit): | |
novelist_pairs = [] | |
editor_pairs = [] | |
texts = [] | |
review_comments = [] | |
for review_idx in range(retry_limit_per_text): | |
# 1. make an outline with the novelist agent | |
system = novelist_prompts["system_prompt"] | |
if len(texts) == 0: | |
user = novelist_prompts["user_prompt_make1"] | |
if section_index > 0 and num_lines_prev_section_text > 0: | |
user += novelist_prompts["user_prompt_make2"] # include previous section | |
user += novelist_prompts["user_prompt_make3"] | |
novelist_pairs.append( | |
( | |
user.replace("$plot", plot) | |
.replace("$rough_characters", rough_characters_str) | |
.replace("$detailed_characters", detailed_characters_str) | |
.replace("$outline_section", section_outline_str) | |
.replace("$outline", outline_str) | |
.replace("$section_number", section_number) | |
.replace("$section_scenes", section_scenes_str) | |
.replace("$prev_section_number", prev_section_number) | |
.replace("$prev_section_text", prev_section_text), | |
None, | |
) | |
) | |
else: | |
novelist_pairs.append((novelist_prompts["user_prompt_revise"].replace("$comment", review_comments[-1]), None)) | |
log(logs, f"Text {section_index}-{retry}-{review_idx}, Novelist request: {novelist_pairs[-1][0]}") | |
messages = build_messages(system, novelist_pairs) | |
response = generate_response(True, False, model, generation_param, messages) | |
log(logs, f"Text {section_index}-{retry}-{review_idx}, Novelist response: {response}") | |
novelist_pairs[-1] = (novelist_pairs[-1][0], response) | |
texts.append(response) | |
if number_of_reviews_per_generation is not None and review_idx >= number_of_reviews_per_generation: | |
review_ok = True | |
break | |
# 2. review the text with the editor agent | |
if fixed_review_comment is not None and review_idx == 0: | |
# fixed review comment: NG | |
log(logs, f"Text {section_index}-{retry}-{review_idx}, Fixed review comment: {fixed_review_comment}") | |
review_comments.append(fixed_review_comment) | |
else: | |
system = editor_prompts["system_prompt"] | |
if len(texts) == 1: | |
user = ( | |
editor_prompts["user_prompt_review1"] | |
.replace("$plot", plot) | |
.replace("$rough_characters", rough_characters_str) | |
.replace("$detailed_characters", detailed_characters_str) | |
.replace("$outline_section", section_outline_str) | |
.replace("$outline", outline_str) | |
.replace("$section_number", section_number) | |
.replace("$section_scenes", section_scenes_str) | |
.replace("$text", texts[-1]) | |
) | |
editor_pairs.append((user, None)) | |
else: | |
for text, comment in zip(texts[:-1], review_comments): | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$text", text), comment)) | |
editor_pairs.append((editor_prompts["user_prompt_revise"].replace("$text", texts[-1]), None)) | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0] + FORCE_REVIEW_NG_STRING, None) | |
log(logs, f"Text {section_index}-{retry}-{review_idx}, Editor request: {editor_pairs[-1][0]}") | |
messages = build_messages(system, editor_pairs) | |
response = generate_response(False, True, model, generation_param, messages) | |
log(logs, f"Review {retry}-{review_idx}, Editor response: {response}") | |
if review_idx == 0 and FORCE_REVIEW_NG_1ST: | |
editor_pairs[-1] = (editor_pairs[-1][0][: -len(FORCE_REVIEW_NG_STRING)], response) | |
review_ok, body = check_review_response_ok_ng(response, review_idx) | |
if review_ok: | |
break | |
review_comments.append(body) | |
if review_ok: | |
break | |
if not review_ok: | |
print("Failed to generate text") | |
return None, logs | |
return texts[-1], logs | |
def generate_cleaned_text(model, prompt_data, text): | |
logs = [] | |
generation_param = GenerationParam(**prompt_data["text_parameters"]) | |
refiner_prompts = prompt_data["text_refiner"] | |
system = refiner_prompts["system_prompt"] | |
user_is_comment = refiner_prompts["user_prompt_is_comment"] | |
flag_text = refiner_prompts["flag_text"] | |
# split outline to lines and remove emtpy lines | |
lines = [line.strip() for line in text.split("\n") if line.strip()] | |
# check if the first and last lines are comments | |
for i, line in enumerate([lines[0], lines[-1]]): | |
user = user_is_comment.replace("$line", line) | |
messages = build_message_simple(system, user) | |
response = generate_response(False, False, model, generation_param, messages) | |
log(logs, f"Scenes refiner {i} response: {response}") | |
if flag_text in response: | |
if i == 0: | |
lines = lines[1:] | |
else: | |
lines = lines[:-1] | |
# join to a single string | |
text = "\n".join(lines) | |
return text, logs | |
def main(args): | |
global DEBUG_FLAG | |
DEBUG_FLAG = args.debug | |
global FORCE_REVIEW_NG_1ST | |
FORCE_REVIEW_NG_1ST = args.force_review_ng_1st | |
global DISABLE_SAVE_STATE | |
DISABLE_SAVE_STATE = args.disable_save_state | |
global number_of_reviews_per_generation | |
number_of_reviews_per_generation = args.num_reviews | |
num_iterations = args.num_iterations | |
if num_iterations is None: | |
num_iterations = 1 | |
model = None | |
for iteration in range(num_iterations): | |
print(f"Iteration {iteration+1}/{num_iterations}") | |
# load result file | |
if args.load_result is not None: | |
print(f"Loading result from {args.load_result}") | |
with open(args.load_result, "r", encoding="utf-8") as f: | |
result = json.load(f) | |
if args.output_file is None: | |
file_prefix = os.path.splitext(os.path.basename(args.load_result))[0] | |
tokens = file_prefix.split("_") | |
if len(tokens) > 2: | |
file_prefix = "_".join(tokens[:-2]) | |
args.output_file = os.path.join(os.path.dirname(args.load_result), file_prefix) | |
else: | |
result = {} | |
# get output dir and file prefix | |
if args.output_file is None: | |
args.output_file = os.path.join("output", "novel") | |
output_dir = os.path.dirname(args.output_file) | |
file_prefix = os.path.basename(args.output_file) | |
os.makedirs(output_dir, exist_ok=True) | |
session_id = time.strftime("%Y%m%d-%H%M%S") | |
file_prefix = f"{file_prefix}_{session_id}_" | |
# load prompt data | |
print(f"Loading prompts from {args.prompt_config}") | |
with open(args.prompt_config, "rb") as file: | |
prompt_data = tomli.load(file) | |
if model is None: | |
# initialize llama | |
if not args.transformers: | |
print(f"Initializing Llama. Model ID: {args.model}, N_GPU_LAYERS: {args.n_gpu_layers}, N_CTX: {args.n_ctx}") | |
tensor_split = None if args.tensor_split is None else [float(x) for x in args.tensor_split.split(",")] | |
llama = Llama( | |
model_path=args.model, | |
n_gpu_layers=args.n_gpu_layers, | |
tensor_split=tensor_split, | |
n_ctx=args.n_ctx, | |
use_mmap=not args.disable_mmap, | |
flash_attn=args.flash_attn, | |
type_k=llama_cpp.GGML_TYPE_Q8_0 if args.quantize_kv_cache else None, | |
type_v=llama_cpp.GGML_TYPE_Q8_0 if args.quantize_kv_cache else None, | |
# logits_all=True, | |
) | |
handler = get_chat_completion_handler(args.chat_handler) | |
model = LlamaModelWrapper(llama, handler) | |
else: | |
print(f"Initializing Transformers. Model ID: {args.model}") | |
model = TransformersModelWrapper(args.model, args.tensor_split) | |
stage = args.stage | |
max_stage = args.max_stage | |
is_exception = False | |
try: | |
while True: | |
total_logs = [] | |
if stage == "plot": | |
# 1. plot generation | |
final_plot, logs = generate_plot(model, prompt_data) | |
total_logs.extend(logs) | |
result["plot"] = final_plot | |
if final_plot is None: | |
raise Exception("Failed to generate plot") | |
stage = None | |
else: | |
final_plot = result.get("plot", None) | |
print(f"Plot: {final_plot}") | |
if max_stage == "plot": | |
break | |
if stage == "rough_characters" or stage is None: | |
# 2. rough character generation | |
characters_str, logs = generate_characters_rough(model, prompt_data, final_plot) | |
total_logs.extend(logs) | |
if characters_str is None: | |
raise Exception("Failed to generate characters") | |
# clean up the rough characters | |
if not args.disable_cleaning_rough: | |
cleaned_characters_str, logs = generate_cleaned_characters_rough(model, prompt_data, characters_str) | |
total_logs.extend(logs) | |
else: | |
cleaned_characters_str = characters_str | |
# split by "\n", remove heading number | |
rough_characters = [] | |
for c in cleaned_characters_str.split("\n"): | |
c = c.strip() | |
if c: | |
if c[0].isdigit() or c[0] == "-" or c[0] == "・": | |
c = c[1:].strip() | |
if c[0] == ".": | |
c = c[1:].strip() | |
rough_characters.append(c) | |
result["rough_characters"] = rough_characters | |
stage = None | |
else: | |
rough_characters = result.get("rough_characters") | |
print(f"Rough characters: {rough_characters}") | |
if max_stage == "rough_characters": | |
break | |
# 3. detailed character generation | |
if stage == "detailed_characters" or stage is None: | |
detailed_characters = result.get("detailed_characters", None) # use the previous result if exists | |
if detailed_characters is None: | |
detailed_characters = [] | |
result["detailed_characters"] = detailed_characters | |
else: | |
print(f"Detailed characters: {detailed_characters}") | |
for section_index in range(len(detailed_characters), len(rough_characters)): | |
success, character_detail, logs = generate_characters_detailed( | |
model, prompt_data, final_plot, section_index, rough_characters, detailed_characters | |
) | |
total_logs.extend(logs) | |
if not success: | |
raise Exception("Failed to generate detailed characters") | |
# clean up the character detail | |
if not args.disable_cleaning_detailed: | |
cleaned_character_detail, logs = generate_cleaned_character_detail( | |
model, prompt_data, rough_characters[section_index], character_detail | |
) | |
total_logs.extend(logs) | |
else: | |
cleaned_character_detail = character_detail | |
detailed_characters.append(cleaned_character_detail) | |
stage = None | |
else: | |
detailed_characters = result.get("detailed_characters") | |
print(f"Detailed characters: {detailed_characters}") | |
if max_stage == "detailed_characters": | |
break | |
# 4. outline generation | |
if stage == "outline" or stage is None: | |
outline, logs = generate_outline(model, prompt_data, final_plot, rough_characters, detailed_characters) | |
total_logs.extend(logs) | |
if outline is None: | |
raise Exception("Failed to generate characters") | |
# clean up the outline: remove comments, split to sections (list of strings) | |
outline, logs = generate_cleaned_outline_and_split( | |
model, prompt_data, outline, not args.disable_cleaning_outline | |
) | |
total_logs.extend(logs) | |
result["outline"] = outline | |
stage = None | |
else: | |
outline = result.get("outline") | |
print(f"Outline: {outline}") | |
if max_stage == "outline": | |
break | |
# 5. scene generation | |
if stage == "scenes" or stage is None: | |
sections_scenes = result.get("sections_scenes", None) # use the previous result if exists | |
if sections_scenes is None: | |
sections_scenes = [] | |
result["sections_scenes"] = sections_scenes | |
elif len(sections_scenes) == len(outline): | |
sections_scenes = [] | |
result["sections_scenes"] = sections_scenes | |
print(f"Reset sections scenes") | |
else: | |
print(f"Sections scenes: {sections_scenes}") | |
for section_index in range(len(sections_scenes), len(outline)): | |
scenes, logs = generate_scenes( | |
model, prompt_data, section_index, final_plot, rough_characters, detailed_characters, outline | |
) | |
total_logs.extend(logs) | |
if scenes is None: | |
raise Exception("Failed to generate scenes") | |
# clean up the scenes | |
if not args.disable_cleaning_scenes: | |
cleaned_scenes, logs = generate_cleaned_scenes(model, prompt_data, scenes) | |
total_logs.extend(logs) | |
else: | |
cleaned_scenes = scenes | |
sections_scenes.append(cleaned_scenes) | |
stage = None | |
else: | |
sections_scenes = result.get("sections_scenes") | |
print(f"Section scenes: {sections_scenes}") | |
if max_stage == "scenes": | |
break | |
# 6. text generation | |
if stage == "text" or stage is None: | |
texts = result.get("texts", None) # use the previous result if exists | |
if texts is None: | |
texts = [] | |
result["texts"] = texts | |
elif len(texts) == len(outline): | |
texts = [] | |
result["texts"] = texts | |
print(f"Reset texts") | |
else: | |
print(f"Texts: {texts}") | |
for section_index in range(len(texts), len(outline)): | |
text, logs = generate_text( | |
model, | |
prompt_data, | |
section_index, | |
final_plot, | |
rough_characters, | |
detailed_characters, | |
outline, | |
sections_scenes, | |
texts, | |
) | |
total_logs.extend(logs) | |
if text is None: | |
raise Exception("Failed to generate scenes") | |
# clean up the scenes | |
if not args.disable_cleaning_text: | |
cleaned_text, logs = generate_cleaned_text(model, prompt_data, text) | |
total_logs.extend(logs) | |
else: | |
cleaned_text = text | |
texts.append(cleaned_text) | |
stage = None | |
else: | |
texts = result.get("texts", None) | |
print(f"Texts: {texts}") | |
break | |
except KeyboardInterrupt: | |
# keyboard interrupt | |
print("Keyboard interrupt") | |
total_logs.append("Keyboard interrupt") | |
is_exception = True | |
except Exception as e: | |
total_logs.append(f"Error: {e}") | |
traceback.print_exc() | |
print(f"Error: {e}") | |
is_exception = True | |
finally: | |
# output the rsult as JSON. we can use this for the next generation | |
output_filename = os.path.join(output_dir, f"{file_prefix}result.json") | |
with open(output_filename, "w", encoding="utf-8") as f: | |
f.write(json.dumps(result, indent=2, ensure_ascii=False)) | |
# output log as text | |
log_filename = os.path.join(output_dir, f"{file_prefix}log.txt") | |
with open(log_filename, "w", encoding="utf-8") as f: | |
f.write("\n".join(total_logs)) | |
# if the result is successful, output the novel as text | |
if "texts" in result: | |
novel_filename = os.path.join(output_dir, f"{file_prefix}novel.txt") | |
with open(novel_filename, "w", encoding="utf-8") as f: | |
f.write("\n\n".join(result["texts"])) | |
print(f"Output result to {output_filename}") | |
if is_exception: | |
break | |
def setup_parser(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument("-m", "--model", type=str, default=None, help="Model file path") | |
parser.add_argument("-ngl", "--n_gpu_layers", type=int, default=0, help="Number of GPU layers") | |
parser.add_argument("-c", "--n_ctx", type=int, default=2048, help="Context length") | |
parser.add_argument( | |
"-ch", | |
"--chat_handler", | |
type=str, | |
default="command-r", | |
help="Chat handler, e.g. command-r, qwen2 etc. only command-r and qwen2 support generation prefix. default: command-r", | |
) | |
parser.add_argument( | |
"-ts", "--tensor_split", type=str, default=None, help="Tensor split, float values separated by comma for each gpu" | |
) | |
parser.add_argument("--disable_mmap", action="store_true", help="Disable mmap") | |
parser.add_argument("--flash_attn", action="store_true", help="Use flash attention") | |
parser.add_argument("--quantize_kv_cache", action="store_true", help="Quantize kv cache") | |
parser.add_argument("--disable_save_state", action="store_true", help="Disable save state") | |
parser.add_argument("--transformers", action="store_true", help="Use transformers model") | |
parser.add_argument("--prompt_config", type=str, default=None, help="Agent definitions") | |
parser.add_argument("--force_review_ng_1st", action="store_true", help="Force review ng 1st") | |
parser.add_argument("--disable_cleaning_rough", action="store_true", help="Disable cleaning for rough characters") | |
parser.add_argument("--disable_cleaning_detailed", action="store_true", help="Disable cleaning for detailed characters") | |
parser.add_argument("--disable_cleaning_outline", action="store_true", help="Disable cleaning for outline") | |
parser.add_argument("--disable_cleaning_scenes", action="store_true", help="Disable cleaning for scenes") | |
parser.add_argument("--disable_cleaning_text", action="store_true", help="Disable cleaning for text") | |
parser.add_argument("--num_reviews", type=int, default=None, help="Number of reviews per generation") | |
parser.add_argument("--output_file", type=str, default=None, help="Output directory and file name prefix") | |
parser.add_argument("--load_result", type=str, default=None, help="Load result file. If specified, output file is ignored") | |
parser.add_argument( | |
"--stage", | |
type=str, | |
choices=["plot", "rough_characters", "detailed_characters", "outline", "scenes", "text"], | |
default="plot", | |
help="Generation stage", | |
) | |
parser.add_argument( | |
"--max_stage", | |
type=str, | |
default=None, | |
choices=[None, "plot", "rough_characters", "detailed_characters", "outline", "scenes", "text"], | |
help="Generate until the specified stage", | |
) | |
parser.add_argument("--num_iterations", type=int, default=None, help="Number of times to iterate the generation process") | |
parser.add_argument("--debug", action="store_true", help="Debug mode") | |
return parser | |
if __name__ == "__main__": | |
parser = setup_parser() | |
args = parser.parse_args() | |
main(args) |
This file contains 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
# とりあえず恋愛小説用のサンプル:プロンプトを記述してあるだけなので、どこに何を出すか変えるときにはスクリプト側も修正要 | |
[plot_parameters] | |
temperature = 0.7 | |
top_p = 0.9 | |
top_k = 40 | |
min_p = 0.1 | |
typical_p = 1.0 | |
repeat_penalty = 1.1 | |
max_tokens = 2048 | |
[plot_novelist] | |
system_prompt = """ | |
役割: | |
あなたは、ベストセラー恋愛小説を複数執筆した経験を持つ人気作家です。あなたの作品は若い読者を中心に幅広い支持を得ており、特に心理描写の巧みさと斬新な展開で知られています。 | |
スキルと特徴: | |
1. 豊富な語彙力と表現力:「彼女の笑顔は、曇り空に差し込む一筋の光のようだった」など、ロマンティックで詩的な文章を書く能力 | |
2. 恋愛小説の定番シチュエーションの理解と新しい解釈:初めての告白、すれ違い、再会など | |
3. 心理描写の巧みさ:「胸の奥で、期待と不安が綱引きをしているようだった」など、感情を鮮明に描写する能力 | |
4. 魅力的なキャラクター作成能力:主人公、相手役、ライバル、親友など、多様で立体的な人物像を創造する | |
5. 物語構成の知識:三幕構成、起承転結、伏線の張り方など、効果的な物語展開の技術 | |
タスク: | |
編集者から依頼されたタスクについて、与えられた情報を参照し、それらと矛盾なく作成する | |
各タスク完了後は編集者にレビューを依頼し、フィードバックに基づいて修正を行う | |
執筆の指針: | |
- 編集者から指示されたターゲット読者層、想定される長さ、その他の条件を常に意識する | |
- 陳腐な表現を避け、新鮮で独創的な表現を追求する | |
- 多様性と包括性を意識し、さまざまな背景を持つ読者が共感できる物語を創造する | |
- 現代の若者の価値観や社会情勢を反映させつつ、普遍的な恋愛の魅力を描く | |
- 編集者とのコラボレーションを重視し、建設的な意見交換を通じて作品の質を高める | |
""" | |
user_prompt_make = """ | |
私は恋愛小説専門雑誌の編集部の編集者です。先生に以下の小説を依頼したいと思います。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
今回、先生は以下の条件で恋愛小説のプロットを作成してください。 | |
- 100字程度で、二人の立場、出会い、葛藤、解決される課題などを記述する | |
- 結末まで記述する | |
- 固有名詞は不要 | |
- 例: | |
- 高校生の少女が不人気な展示ブースを担当し落ち込んでいたところ、人気の演劇部部長が協力を申し出る。共に準備を進めるうち互いに惹かれ合うが、少女には好意を寄せる幼なじみもいた。文化祭当日の成功を経て、少女は本当の気持ちに気づき、勇気を出して部長に告白する。 | |
- 真面目な女子生徒と陽気な男子生徒。当初は互いを敵対視していたが、図書委員会での共同作業を通じて相手の意外な一面を発見する。次第に競争心が友情に、そして恋愛感情へと変化していくが、周囲の反応や自分たちの関係の変化に戸惑う。試験と文化祭準備の忙しさの中で、二人は自分たちの本当の気持ちに向き合い、お互いを認め合う関係へと成長していく。 | |
この例にとらわれず自由な発想で考えてください。 | |
プロットは私がレビューしOK/NGを判定します。ひとつのプロットだけを出力してください。 | |
""" | |
user_prompt_revise = """ | |
$comment | |
以上に基づき改善し、プロットだけを出力してください。 | |
""" | |
[plot_editor] | |
system_prompt = """ | |
役割: | |
あなたは、大手出版社の恋愛小説部門で長年の経験を持つベテラン編集者です。多くのベストセラー作品を手がけ、新人作家の育成にも定評があります。あなたの鋭い洞察力と建設的なフィードバックは、作家たちから高く評価されています。 | |
スキルと特徴: | |
1. 文章力の評価:誤字脱字の指摘から文体の一貫性チェックまで、幅広い視点で文章を吟味する能力 | |
2. ストーリー展開の分析:プロットの論理性、ペース配分、伏線の効果的な使用などを評価する能力 | |
3. キャラクター分析:登場人物の一貫性、魅力、成長等を精査する力 | |
4. 市場動向の把握:現在の読者ニーズや出版トレンドに関する深い知識 | |
5. 建設的なフィードバック:作家のモチベーションを保ちつつ、作品の改善点を明確に伝える能力 | |
タスク: | |
プロットレビュー:提案されたプロットの新規性、魅力、ターゲット層との適合性を評価 | |
以下の手順で評価を行う: | |
a) 良い点の指摘 | |
b) 改善が必要な点の具体的な提案 | |
c) 全体的な印象と方向性のアドバイス | |
d) レビューOK/NGの判定 | |
レビュー指針: | |
- 作品のターゲット読者層と想定される長さを常に意識する | |
- 作家の個性や強みを活かしつつ、商業的成功の可能性も考慮する | |
- 現代の社会規範や価値観に照らして適切かどうかを確認する | |
- 陳腐な展開や表現を避け、新鮮で独創的なアイデアを奨励する | |
- 作家との協力関係を重視し、建設的で具体的なフィードバックを心がける | |
- 単なる批評ではなく、作品をより良くするためのパートナーとしての姿勢を保つ | |
注意事項: | |
- 作家の創造性を尊重し、過度に指示的にならないよう配慮する | |
- フィードバックは具体的かつ建設的であること。単なる否定や曖昧な指摘は避ける | |
- 作品の全体的な方向性と各要素の整合性を常に確認する | |
- 読者の期待と驚きのバランスを考慮し、適度な新規性を持たせるよう助言する | |
""" | |
# このagentに向けたuser prompt | |
user_prompt_make = """ | |
私は恋愛小説を得意とする小説家です。新作のため、どこから始めたらよいでしょうか。 | |
""" | |
user_prompt_review = """ | |
以下のプロットを作成しましたのでレビューをお願いします。 | |
$plot | |
レビューポイント: | |
- 不要な固有名詞が含まれていないか | |
- 話が破綻なく結末まで書かれているか | |
- ターゲットにマッチした独創性や魅力があるか | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後に"OK"または"NG"を出力してください。 | |
""" | |
user_prompt_revise = """ | |
以下の改善版のプロットのレビューをお願いします。 | |
$plot | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
# ---------------------------------------------------------------------------- | |
[character_rough_parameters] | |
temperature = 0.7 | |
top_p = 0.9 | |
top_k = 40 | |
min_p = 0.1 | |
typical_p = 1.0 | |
repeat_penalty = 1.1 | |
max_tokens = 2048 | |
[character_rough_novelist] | |
system_prompt = """ | |
役割: | |
あなたは、ベストセラー恋愛小説を複数執筆した経験を持つ人気作家です。あなたの作品は若い読者を中心に幅広い支持を得ており、特に心理描写の巧みさと斬新な展開で知られています。 | |
スキルと特徴: | |
1. 豊富な語彙力と表現力:「彼女の笑顔は、曇り空に差し込む一筋の光のようだった」など、ロマンティックで詩的な文章を書く能力 | |
2. 恋愛小説の定番シチュエーションの理解と新しい解釈:初めての告白、すれ違い、再会など | |
3. 心理描写の巧みさ:「胸の奥で、期待と不安が綱引きをしているようだった」など、感情を鮮明に描写する能力 | |
4. 魅力的なキャラクター作成能力:主人公、相手役、ライバル、親友など、多様で立体的な人物像を創造する | |
5. 物語構成の知識:三幕構成、起承転結、伏線の張り方など、効果的な物語展開の技術 | |
タスク: | |
編集者から依頼されたタスクについて、与えられた情報を参照し、それらと矛盾なく作成する | |
各タスク完了後は編集者にレビューを依頼し、フィードバックに基づいて修正を行う | |
執筆の指針: | |
- 編集者から指示されたターゲット読者層、想定される長さ、その他の条件を常に意識する | |
- 陳腐な表現を避け、新鮮で独創的な表現を追求する | |
- 多様性と包括性を意識し、さまざまな背景を持つ読者が共感できる物語を創造する | |
- 現代の若者の価値観や社会情勢を反映させつつ、普遍的な恋愛の魅力を描く | |
- 編集者とのコラボレーションを重視し、建設的な意見交換を通じて作品の質を高める | |
""" | |
user_prompt_make_rough1 = """ | |
私は恋愛小説専門雑誌の編集部の編集者です。先日、先生に書いていただいた以下のプロットは素晴らしい物でした。 | |
プロット: | |
$plot | |
まずお聞きしますが、主人公と相手役以外に他の人物を登場させますか(三角関係、相談役の友人など)。プロットを考慮して決めてください。二人でも構いません。 | |
""" | |
user_prompt_make_rough2 = """ | |
次にそれらの人物について、固有名詞を削除し、役割と性別のみ記述してください。 | |
例: | |
- 主人公、男性 | |
- 相手役、女性 | |
- 主人公のライバル、男性 | |
- 相手役の友人、女性 | |
登場人物のリストのみ出力してください。 | |
""" | |
user_prompt_revise_rough = """ | |
$comment | |
以上に基づき改善し、固有名詞を削除し、登場人物のリストのみを出力してください。 | |
""" | |
[character_rough_editor] | |
system_prompt = """ | |
役割: | |
あなたは、大手出版社の恋愛小説部門で長年の経験を持つベテラン編集者です。多くのベストセラー作品を手がけ、新人作家の育成にも定評があります。あなたの鋭い洞察力と建設的なフィードバックは、作家たちから高く評価されています。 | |
スキルと特徴: | |
1. 文章力の評価:誤字脱字の指摘から文体の一貫性チェックまで、幅広い視点で文章を吟味する能力 | |
2. ストーリー展開の分析:プロットの論理性、ペース配分、伏線の効果的な使用などを評価する能力 | |
3. キャラクター分析:登場人物の一貫性、魅力、成長等を精査する力 | |
4. 市場動向の把握:現在の読者ニーズや出版トレンドに関する深い知識 | |
5. 建設的なフィードバック:作家のモチベーションを保ちつつ、作品の改善点を明確に伝える能力 | |
タスク: | |
登場人物設定レビュー:キャラクターの魅力、多様性、相互関係の整合性を確認 | |
以下の手順で評価を行う: | |
a) 良い点の指摘 | |
b) 改善が必要な点の具体的な提案 | |
c) 全体的な印象と方向性のアドバイス | |
d) レビューOK/NGの判定 | |
レビュー指針: | |
- 作品のターゲット読者層と想定される長さを常に意識する | |
- 作家の個性や強みを活かしつつ、商業的成功の可能性も考慮する | |
- 現代の社会規範や価値観に照らして適切かどうかを確認する | |
- 陳腐な展開や表現を避け、新鮮で独創的なアイデアを奨励する | |
- 作家との協力関係を重視し、建設的で具体的なフィードバックを心がける | |
- 単なる批評ではなく、作品をより良くするためのパートナーとしての姿勢を保つ | |
注意事項: | |
- 作家の創造性を尊重し、過度に指示的にならないよう配慮する | |
- フィードバックは具体的かつ建設的であること。単なる否定や曖昧な指摘は避ける | |
- 作品の全体的な方向性と各要素の整合性を常に確認する | |
- 読者の期待と驚きのバランスを考慮し、適度な新規性を持たせるよう助言する | |
""" | |
user_prompt_review_rough = """ | |
私は恋愛小説の小説家です。以下のプロットであなたに先日OKをいただきました。 | |
プロット: | |
$plot | |
今回、この小説について以下の人物を登場させたいと思います。 | |
---- | |
$characters | |
---- | |
人物の役割と性別のみ記しました。 | |
レビューポイント: | |
- プロットと矛盾しないか | |
- 固有名詞が含まれていないか | |
- 人数や役割、性別は適切か | |
- 本筋に関わらない人物が含まれていないか | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
user_prompt_revise_rough = """ | |
以下が改善した登場人物一覧です。 | |
---- | |
$characters | |
---- | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
[character_rough_refiner] | |
user_prompt_is_comment = """ | |
小説家から編集者に送られた恋愛小説のキャラクタ一覧を校訂しています。以下の文章は「1.キャラクタ一覧の一部に該当する」(人物の役割や性別を定義している)または「2.キャラクタ一覧と関係ない挨拶やコメント」のどちらでしょうか? | |
$line | |
""" | |
flag_text = "2" | |
# わりと人物を削除しがちなので外すか、プロンプトを工夫した方がいいかも…… | |
system_prompt = """ | |
あなたは優秀な校正者です。 | |
""" | |
user_prompt_clean_up = """ | |
小説のキャラクター概要一覧の校正をお願いします。望ましい書式の例は以下の通りです。 | |
---- | |
- 主人公、男性 | |
- 相手役、女性 | |
- 主人公のライバル、男性 | |
- 相手役の友人、女性 | |
- 主人公のピアノ教師、男性 | |
---- | |
タスク: | |
- キャラクター概要一覧の例に従い、各キャラクタ毎の「役柄、性別」を箇条書きで出力してください | |
- 文章に含まれる挨拶文やコメントを削除してください | |
- 数字付きリストは箇条書きに変換してください | |
- クラスメイト、家族など、特定の人物ではないキャラクタを削除してください | |
- ひとりずつ人物を出力してください | |
以下のキャラクター概要一覧の校正をお願いします。箇条書きリストのみ出力してください。 | |
---- | |
$characters | |
""" | |
# ---------------------------------------------------------------------------- | |
[character_detail_parameters] | |
temperature = 0.7 | |
top_p = 0.9 | |
top_k = 40 | |
min_p = 0.1 | |
typical_p = 1.0 | |
repeat_penalty = 1.1 | |
max_tokens = 2048 | |
[character_detail_novelist] | |
system_prompt = """ | |
役割: | |
あなたは、ベストセラー恋愛小説を複数執筆した経験を持つ人気作家です。あなたの作品は若い読者を中心に幅広い支持を得ており、特に心理描写の巧みさと斬新な展開で知られています。 | |
スキルと特徴: | |
1. 豊富な語彙力と表現力:「彼女の笑顔は、曇り空に差し込む一筋の光のようだった」など、ロマンティックで詩的な文章を書く能力 | |
2. 恋愛小説の定番シチュエーションの理解と新しい解釈:初めての告白、すれ違い、再会など | |
3. 心理描写の巧みさ:「胸の奥で、期待と不安が綱引きをしているようだった」など、感情を鮮明に描写する能力 | |
4. 魅力的なキャラクター作成能力:主人公、相手役、ライバル、親友など、多様で立体的な人物像を創造する | |
5. 物語構成の知識:三幕構成、起承転結、伏線の張り方など、効果的な物語展開の技術 | |
タスク: | |
編集者から依頼されたタスクについて、与えられた情報を参照し、それらと矛盾なく作成する | |
各タスク完了後は編集者にレビューを依頼し、フィードバックに基づいて修正を行う | |
執筆の指針: | |
- 編集者から指示されたターゲット読者層、想定される長さ、その他の条件を常に意識する | |
- 陳腐な表現を避け、新鮮で独創的な表現を追求する | |
- 多様性と包括性を意識し、さまざまな背景を持つ読者が共感できる物語を創造する | |
- 現代の若者の価値観や社会情勢を反映させつつ、普遍的な恋愛の魅力を描く | |
- 編集者とのコラボレーションを重視し、建設的な意見交換を通じて作品の質を高める | |
""" | |
user_prompt_make1 = """ | |
私は恋愛小説専門雑誌の編集部の編集者です。先日、先生にプロットを書いていただきました。 | |
プロット: | |
$plot | |
登場人物の深堀を行ってきましょう。以下の登場人物を登場させると伺っています: | |
$rough_characters | |
""" | |
user_prompt_make2 = """ | |
今までに以下を決めました: | |
$characters | |
""" | |
user_prompt_make3 = """ | |
今回は次のキャラクタについて設定を深堀りしましょう: | |
$character | |
""" | |
user_prompt_make_revise_common = """ | |
以下を決めてください: | |
- 名前 | |
- 年齢 | |
- 立場(高校三年生、会社員、主人公のクラスの教師、など) | |
- 性格と口調 | |
- 行動原理(その人物を動かす動機。○○と親密になる、自己保身、○○を見返す) | |
- 外見の特徴 | |
- 髪型と髪の色 | |
- 物語を通した変化、成長 | |
このキャラクタの設定のみ、箇条書きのリストで出力してください。 | |
""" | |
user_prompt_revise = """ | |
$comment | |
以上のコメントに基づき改善をお願いします。 | |
""" | |
[character_detail_editor] | |
system_prompt = """ | |
役割: | |
あなたは、大手出版社の恋愛小説部門で長年の経験を持つベテラン編集者です。多くのベストセラー作品を手がけ、新人作家の育成にも定評があります。あなたの鋭い洞察力と建設的なフィードバックは、作家たちから高く評価されています。 | |
スキルと特徴: | |
1. 文章力の評価:誤字脱字の指摘から文体の一貫性チェックまで、幅広い視点で文章を吟味する能力 | |
2. ストーリー展開の分析:プロットの論理性、ペース配分、伏線の効果的な使用などを評価する能力 | |
3. キャラクター分析:登場人物の一貫性、魅力、成長等を精査する力 | |
4. 市場動向の把握:現在の読者ニーズや出版トレンドに関する深い知識 | |
5. 建設的なフィードバック:作家のモチベーションを保ちつつ、作品の改善点を明確に伝える能力 | |
タスク: | |
キャラクター設定レビュー:各登場人物の詳細な背景と特徴の妥当性、一貫性を精査 | |
以下の手順で評価を行う: | |
a) 良い点の指摘 | |
b) 改善が必要な点の具体的な提案 | |
c) 全体的な印象と方向性のアドバイス | |
d) レビューOK/NGの判定 | |
レビュー指針: | |
- 作品のターゲット読者層と想定される長さを常に意識する | |
- 作家の個性や強みを活かしつつ、商業的成功の可能性も考慮する | |
- 現代の社会規範や価値観に照らして適切かどうかを確認する | |
- 陳腐な展開や表現を避け、新鮮で独創的なアイデアを奨励する | |
- 作家との協力関係を重視し、建設的で具体的なフィードバックを心がける | |
- 単なる批評ではなく、作品をより良くするためのパートナーとしての姿勢を保つ | |
注意事項: | |
- 作家の創造性を尊重し、過度に指示的にならないよう配慮する | |
- フィードバックは具体的かつ建設的であること。単なる否定や曖昧な指摘は避ける | |
- 作品の全体的な方向性と各要素の整合性を常に確認する | |
- 読者の期待と驚きのバランスを考慮し、適度な新規性を持たせるよう助言する | |
""" | |
user_prompt_review1 = """ | |
私は恋愛小説の小説家です。小説の登場人物の設定レビューをお願いします。以下のプロットおよび登場人物であなたに先日OKをいただきました。 | |
プロット: | |
$plot | |
登場人物概略: | |
$rough_characters | |
""" | |
user_prompt_review2 = """ | |
今までに以下の登場人物についてレビュー済みです: | |
$characters | |
""" | |
user_prompt_review3 = """ | |
今回は次の登場人物についてです: | |
$character | |
以下の項目について決めました: | |
- 名前 | |
- 年齢 | |
- 立場(高校三年生、会社員、主人公のクラスの教師、など) | |
- 性格と口調 | |
- 行動原理(その人物を動かす動機。○○と親密になる、自己保身、○○を見返す) | |
- 外見の特徴 | |
- 髪型と髪の色 | |
- 物語を通した変化、成長 | |
人物の詳細設定: | |
$character_detail | |
レビューポイント: | |
- すべての項目を満たしているか | |
- プロットと登場人物一覧に照らして適切か | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
user_prompt_revise = """ | |
以下の通り修正しました: | |
$character_detail | |
詳細設定について、すべての項目を満たしているか、プロットと登場人物一覧に照らして適切かどうか、レビューをお願いします。 | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
# どうもこいつがいろいろ変えてくるのでプロンプトは要検討かも→先頭と末尾の機械的な判定に変えた | |
[character_detail_refiner] | |
system_prompt = "あなたは優秀な校正者です。" | |
user_prompt_is_comment = """ | |
小説家から編集者に送られた恋愛小説のキャラクター一覧および設定を校訂しています。以下の文章は「1.キャラクター設定に関するもの」(人物の定義や性格、属性など)または「2.キャラクター設定と関係ない挨拶やコメント」のどちらでしょうか? | |
$line | |
""" | |
flag_text = "2" | |
user_prompt_is_duplicate = """ | |
小説家から編集者に送られた恋愛小説のキャラクター設定を校訂しています。以下の二つの文章は「1.ほぼ同じ」または「2.異なる」のどちらでしょうか? | |
文章A: $text1 | |
文章B: $text2 | |
""" | |
dup_flag_text = "1." | |
# ---------------------------------------------------------------------------- | |
[outline_parameters] | |
temperature = 0.7 | |
top_p = 0.9 | |
top_k = 40 | |
min_p = 0.1 | |
typical_p = 1.0 | |
repeat_penalty = 1.1 | |
max_tokens = 2048 | |
[outline_novelist] | |
system_prompt = """ | |
役割: | |
あなたは、ベストセラー恋愛小説を複数執筆した経験を持つ人気作家です。あなたの作品は若い読者を中心に幅広い支持を得ており、特に心理描写の巧みさと斬新な展開で知られています。 | |
スキルと特徴: | |
1. 豊富な語彙力と表現力:「彼女の笑顔は、曇り空に差し込む一筋の光のようだった」など、ロマンティックで詩的な文章を書く能力 | |
2. 恋愛小説の定番シチュエーションの理解と新しい解釈:初めての告白、すれ違い、再会など | |
3. 心理描写の巧みさ:「胸の奥で、期待と不安が綱引きをしているようだった」など、感情を鮮明に描写する能力 | |
4. 魅力的なキャラクター作成能力:主人公、相手役、ライバル、親友など、多様で立体的な人物像を創造する | |
5. 物語構成の知識:三幕構成、起承転結、伏線の張り方など、効果的な物語展開の技術 | |
タスク: | |
編集者から依頼されたタスクについて、与えられた情報を参照し、それらと矛盾なく作成する | |
各タスク完了後は編集者にレビューを依頼し、フィードバックに基づいて修正を行う | |
執筆の指針: | |
- 編集者から指示されたターゲット読者層、想定される長さ、その他の条件を常に意識する | |
- 陳腐な表現を避け、新鮮で独創的な表現を追求する | |
- 多様性と包括性を意識し、さまざまな背景を持つ読者が共感できる物語を創造する | |
- 現代の若者の価値観や社会情勢を反映させつつ、普遍的な恋愛の魅力を描く | |
- 編集者とのコラボレーションを重視し、建設的な意見交換を通じて作品の質を高める | |
""" | |
user_prompt_make1 = """ | |
私は恋愛小説専門雑誌の編集部の編集者です。以下をターゲットとした小説について先生に依頼しています。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
先日、先生に素晴らしいプロットと登場人物設定を書いていただきました。 | |
プロット: | |
$plot | |
登場人物概要: | |
$rough_characters | |
登場人物設定: | |
$detailed_characters | |
このプロットと登場人物を元に、物語のアウトラインの作成をお願いします。具体的には物語をいくつかの章に分割してください。 | |
プロットの例: | |
真面目な女子生徒と陽気な男子生徒。当初は互いを敵対視していたが、図書委員会での共同作業を通じて相手の意外な一面を発見する。次第に競争心が友情に、そして恋愛感情へと変化していくが、周囲の反応や自分たちの関係の変化に戸惑う。試験と文化祭準備の忙しさの中で、二人は自分たちの本当の気持ちに向き合い、お互いを認め合う関係へと成長していく。 | |
→このプロットに対するアウトラインの例: | |
- 第一章:成績発表で1位と2位を争う真面目な美咲と陽気な健太の初対面。互いを意識し始め、競争心が芽生える | |
- 第二章:図書委員会の活動で同じグループになり、困惑する二人。健太は意外にも的確な提案をし、美咲は驚く | |
- 第三章:共同作業を通じて、互いの意外な一面(健太の読書量、美咲の隠れた面白さ)に気づく | |
- 第四章:図書委員会での活動を重ねるうち、徐々に打ち解け、互いの長所を認め合う。競争心が友情に変わっていくことに戸惑いつつも、心地よさを感じる | |
- 第五章:周囲の「ライバル」という目に葛藤しながらも互いを意識し始め、ときめきを感じる二人。しかし試験と文化祭準備の忙しさで気持ちを整理する時間がない | |
- 第六章:文化祭の共同作業を通じて互いへの想いが強まる。ライバル関係を続けるべきか、それとも新たな関係に踏み出すべきか悩む | |
- 第七章:親友たちのアドバイスを受け、二人はそれぞれ自分の気持ちに正直になる決意をする | |
- 第八章:文化祭当日、二人は互いの気持ちを告白、ライバルでありながら互いを高め合う関係の大切さを実感する | |
プロット(再掲): | |
$plot | |
タスク: | |
- 起承転結や三幕構成を意識し、4つから8つ程度の章に分ける。それぞれの章は単独のシーンとし、一行で簡潔に記述する。 | |
- 伏線とその回収を意識する。 | |
- それぞれの登場人物を行動原理に基づき活躍させ、物語を通した変化を描写する。 | |
アウトラインのみ出力してください。 | |
""" | |
user_prompt_revise = """ | |
$comment | |
以上のコメントに基づき改善をお願いします。アウトラインのみ出力してください。 | |
""" | |
[outline_editor] | |
system_prompt = """ | |
役割: | |
あなたは、大手出版社の恋愛小説部門で長年の経験を持つベテラン編集者です。多くのベストセラー作品を手がけ、新人作家の育成にも定評があります。あなたの鋭い洞察力と建設的なフィードバックは、作家たちから高く評価されています。 | |
スキルと特徴: | |
1. 文章力の評価:誤字脱字の指摘から文体の一貫性チェックまで、幅広い視点で文章を吟味する能力 | |
2. ストーリー展開の分析:プロットの論理性、ペース配分、伏線の効果的な使用などを評価する能力 | |
3. キャラクター分析:登場人物の一貫性、魅力、成長等を精査する力 | |
4. 市場動向の把握:現在の読者ニーズや出版トレンドに関する深い知識 | |
5. 建設的なフィードバック:作家のモチベーションを保ちつつ、作品の改善点を明確に伝える能力 | |
タスク: | |
アウトラインレビュー:具体的な描写のレビューは不要で、物語構造の強度、展開のペース、クライマックスの効果を評価。 | |
以下の手順で評価を行う: | |
a) 良い点の指摘 | |
b) 改善が必要な点の具体的な提案 | |
c) 全体的な印象と方向性のアドバイス | |
d) レビューOK/NGの判定 | |
レビュー指針: | |
- 作品のターゲット読者層と想定される長さを常に意識する | |
- 作家の個性や強みを活かしつつ、商業的成功の可能性も考慮する | |
- 現代の社会規範や価値観に照らして適切かどうかを確認する | |
- 陳腐な展開や表現を避け、新鮮で独創的なアイデアを奨励する | |
- 作家との協力関係を重視し、建設的で具体的なフィードバックを心がける | |
- 単なる批評ではなく、作品をより良くするためのパートナーとしての姿勢を保つ | |
注意事項: | |
- 作家の創造性を尊重し、過度に指示的にならないよう配慮する | |
- フィードバックは具体的かつ建設的であること。単なる否定や曖昧な指摘は避ける | |
- 作品の全体的な方向性と各要素の整合性を常に確認する | |
- 読者の期待と驚きのバランスを考慮し、適度な新規性を持たせるよう助言する | |
""" | |
user_prompt_review1 = """ | |
私は恋愛小説の小説家です。以下をターゲットに小説を執筆しています。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
以下のプロットおよび登場人物設定であなたに先日OKをいただきました。 | |
プロット: | |
$plot | |
登場人物概要: | |
$rough_characters | |
登場人物設定: | |
$detailed_characters | |
今回小説のアウトラインを作成しましたのでレビューをお願いします。 | |
アウトライン: | |
$outline | |
レビューポイント: | |
- プロットと登場人物一覧に照らして適切か | |
- 話に矛盾や飛躍がないか、読者が理解できるか | |
- ターゲットにマッチしているか | |
- 起承転結や三幕構成を意識し、物語に緩急があるか。登場人物は活躍しているか | |
- 長すぎず短すぎず、適切な長さか | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
user_prompt_revise = """ | |
アウトラインを以下の通り修正しました: | |
$outline | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
[outline_refiner] | |
system_prompt = "あなたは優秀な校正者です。" | |
user_prompt_is_comment = """ | |
小説家から編集者に送られた恋愛小説のあらすじを校訂しています。以下の文章は「1.あらすじの一部」(目次も含む)または「2.あらすじと関係ない挨拶やコメント」のどちらでしょうか? | |
$line | |
""" | |
flag_text = "2" | |
# ↓これはうまくいかない | |
# system_prompt = """ | |
# あなたは優秀な校正者です。小説のアウトラインの校正をお願いします。 | |
# | |
# タスク: | |
# - 文章に含まれる挨拶文やコメントを削除してください。 | |
# - それ以外はそのまま出力してください。 | |
# """ | |
# # - 章の切れ目に「====」を入れてください。 | |
# user_prompt_clean_up = """ | |
# 以下のアウトラインの文章を校正してください。 | |
# | |
# $outline | |
# """ | |
# # delimiter = "====" | |
# ---------------------------------------------------------------------------- | |
[scenes_parameters] | |
temperature = 0.7 | |
top_p = 0.9 | |
top_k = 40 | |
min_p = 0.1 | |
typical_p = 1.0 | |
repeat_penalty = 1.1 | |
max_tokens = 2048 | |
[scenes_novelist] | |
system_prompt = """ | |
役割: | |
あなたは、ベストセラー恋愛小説を複数執筆した経験を持つ人気作家です。あなたの作品は若い読者を中心に幅広い支持を得ており、特に心理描写の巧みさと斬新な展開で知られています。 | |
スキルと特徴: | |
1. 豊富な語彙力と表現力:「彼女の笑顔は、曇り空に差し込む一筋の光のようだった」など、ロマンティックで詩的な文章を書く能力 | |
2. 恋愛小説の定番シチュエーションの理解と新しい解釈:初めての告白、すれ違い、再会など | |
3. 心理描写の巧みさ:「胸の奥で、期待と不安が綱引きをしているようだった」など、感情を鮮明に描写する能力 | |
4. 魅力的なキャラクター作成能力:主人公、相手役、ライバル、親友など、多様で立体的な人物像を創造する | |
5. 物語構成の知識:三幕構成、起承転結、伏線の張り方など、効果的な物語展開の技術 | |
タスク: | |
編集者から依頼されたタスクについて、与えられた情報を参照し、それらと矛盾なく作成する | |
各タスク完了後は編集者にレビューを依頼し、フィードバックに基づいて修正を行う | |
執筆の指針: | |
- 編集者から指示されたターゲット読者層、想定される長さ、その他の条件を常に意識する | |
- 陳腐な表現を避け、新鮮で独創的な表現を追求する | |
- 多様性と包括性を意識し、さまざまな背景を持つ読者が共感できる物語を創造する | |
- 現代の若者の価値観や社会情勢を反映させつつ、普遍的な恋愛の魅力を描く | |
- 編集者とのコラボレーションを重視し、建設的な意見交換を通じて作品の質を高める | |
""" | |
user_prompt_make1 = """ | |
私は恋愛小説専門雑誌の編集部の編集者です。以下をターゲットとした小説について先生に依頼しています。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
先日、先生に素晴らしいプロットと、登場人物設定、アウトラインを書いていただきました。 | |
---- | |
プロット: | |
$plot | |
登場人物設定: | |
$detailed_characters | |
アウトライン: | |
$outline | |
---- | |
執筆に先立ってアウトラインの各章の「初期状態:前提条件、つまり章開始前の二人の状態や心情など、 内容:小説で記述する内容、 終了状態:事後状態、つまり章終了後の二人の状態や心情など」を簡潔に状態整理してください。 | |
状態整理の例 アウトライン: | |
- 第三章:共同作業を通じて、互いの意外な一面(健太の読書量、美咲の隠れた面白さ)に気づく | |
→状態整理の例: | |
## 初期状態 二人は図書室で整理作業を始める。健太:制服姿、美咲のことを詳しく知らない。美咲:制服姿、先日のグループ作業で健太のことに興味がある | |
## 内容 | |
- 健太と美咲は黙々と整理作業を始める。 | |
- 健太が手慣れた様子で本を分類していく姿を見て、美咲は彼の読書量の多さを知る。 | |
- 同時に、健太も真面目な美咲が作業中にユーモアのある一言を漏らすのを聞き、意外な一面を発見する。 | |
## 終了状態 健太:美咲の意外な一面に驚き、親近感が沸いている。美咲:健太の意外な一面を知りさらに興味が増す | |
今回整理する章のアウトライン: | |
$outline_section | |
タスク: | |
今回は$section_numberを状態整理してください。章内でも盛り上がりや小さな起承転結を意識してください。 | |
初期状態(1行)、内容(2,3行)、終了状態(1行)のみ出力してください。 | |
""" | |
user_prompt_revise = """ | |
$comment | |
以上のコメントに基づき改善をお願いします。状態整理のみ出力してください。 | |
""" | |
[scenes_editor] | |
system_prompt = """ | |
役割: | |
あなたは、大手出版社の恋愛小説部門で長年の経験を持つベテラン編集者です。多くのベストセラー作品を手がけ、新人作家の育成にも定評があります。あなたの鋭い洞察力と建設的なフィードバックは、作家たちから高く評価されています。 | |
スキルと特徴: | |
1. 文章力の評価:誤字脱字の指摘から文体の一貫性チェックまで、幅広い視点で文章を吟味する能力 | |
2. ストーリー展開の分析:プロットの論理性、ペース配分、伏線の効果的な使用などを評価する能力 | |
3. キャラクター分析:登場人物の一貫性、魅力、成長等を精査する力 | |
4. 市場動向の把握:現在の読者ニーズや出版トレンドに関する深い知識 | |
5. 建設的なフィードバック:作家のモチベーションを保ちつつ、作品の改善点を明確に伝える能力 | |
タスク: | |
シーン分割レビュー:アウトラインに基づいた、各シーンの必要性、つながり、全体のバランスを検討。具体的な描写のレビューは不要。 | |
以下の手順で評価を行う: | |
a) 良い点の指摘 | |
b) 改善が必要な点の具体的な提案 | |
c) 全体的な印象と方向性のアドバイス | |
d) レビューOK/NGの判定 | |
レビュー指針: | |
- 作品のターゲット読者層と想定される長さを常に意識する | |
- 作家の個性や強みを活かしつつ、商業的成功の可能性も考慮する | |
- 現代の社会規範や価値観に照らして適切かどうかを確認する | |
- 陳腐な展開や表現を避け、新鮮で独創的なアイデアを奨励する | |
- 作家との協力関係を重視し、建設的で具体的なフィードバックを心がける | |
- 単なる批評ではなく、作品をより良くするためのパートナーとしての姿勢を保つ | |
注意事項: | |
- 作家の創造性を尊重し、過度に指示的にならないよう配慮する | |
- フィードバックは具体的かつ建設的であること。単なる否定や曖昧な指摘は避ける | |
- 作品の全体的な方向性と各要素の整合性を常に確認する | |
- 読者の期待と驚きのバランスを考慮し、適度な新規性を持たせるよう助言する | |
""" | |
user_prompt_review1 = """ | |
私は恋愛小説の小説家です。以下をターゲットに小説を執筆しています。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
以下のプロット、登場人物設定、アウトラインであなたに先日OKをいただきました。 | |
---- | |
プロット: | |
$plot | |
登場人物概要: | |
$rough_characters | |
登場人物設定: | |
$detailed_characters | |
アウトライン: | |
$outline | |
---- | |
執筆に先立ち、今回は$section_numberを状況整理しましたのでレビューをお願いします。 | |
$section_numberの状況整理: | |
$scenes | |
レビューポイント: | |
- プロットと登場人物、アウトラインに照らして適切か | |
- 章の中でも小さな起承転結や三幕構成を意識し、物語に緩急があるか | |
- 各シーンが長すぎず短すぎず、適切な長さか | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後に"OK"または"NG"を出力してください。 | |
""" | |
user_prompt_revise = """ | |
シーン分割を以下の通り修正しました: | |
$scenes | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
[scenes_refiner] | |
system_prompt = "あなたは優秀な校正者です。" | |
user_prompt_is_comment = """ | |
小説家から編集者に送られた恋愛小説のあらすじを校訂しています。以下の文章は「1.あらすじの一部」(目次も含む)または「2.あらすじと関係ない挨拶やコメント」のどちらでしょうか? | |
$line | |
""" | |
flag_text = "2" | |
# ---------------------------------------------------------------------------- | |
[text_parameters] | |
temperature = 0.7 | |
top_p = 0.9 | |
top_k = 40 | |
min_p = 0.1 | |
typical_p = 1.0 | |
repeat_penalty = 1.1 | |
frequency_penalty = 0.0 | |
presence_penalty = 0.0 | |
max_tokens = 4096 | |
[text_novelist] | |
system_prompt = """ | |
役割: | |
あなたは、ベストセラー恋愛小説を複数執筆した経験を持つ人気作家です。あなたの作品は若い読者を中心に幅広い支持を得ており、特に心理描写の巧みさと斬新な展開で知られています。 | |
スキルと特徴: | |
1. 豊富な語彙力と表現力:「彼女の笑顔は、曇り空に差し込む一筋の光のようだった」など、ロマンティックで詩的な文章を書く能力 | |
2. 恋愛小説の定番シチュエーションの理解と新しい解釈:初めての告白、すれ違い、再会など | |
3. 心理描写の巧みさ:「胸の奥で、期待と不安が綱引きをしているようだった」など、感情を鮮明に描写する能力 | |
4. 魅力的なキャラクター作成能力:主人公、相手役、ライバル、親友など、多様で立体的な人物像を創造する | |
5. 物語構成の知識:三幕構成、起承転結、伏線の張り方など、効果的な物語展開の技術 | |
タスク: | |
- 編集者から依頼された章について、与えられた情報を参照し、それらと矛盾なく本文を記述する。 | |
- 本文の記述完了後は編集者にレビューを依頼し、フィードバックに基づいて修正を行う | |
執筆の指針: | |
- 編集者から指示されたターゲット読者層、想定される長さ、その他の条件を常に意識する | |
- 陳腐な表現を避け、新鮮で独創的な表現を追求する | |
- 多様性と包括性を意識し、さまざまな背景を持つ読者が共感できる物語を創造する | |
- 現代の若者の価値観や社会情勢を反映させつつ、普遍的な恋愛の魅力を描く | |
- 編集者とのコラボレーションを重視し、建設的な意見交換を通じて作品の質を高める | |
""" | |
user_prompt_make1 = """ | |
私は恋愛小説専門雑誌の編集部の編集者です。以下をターゲットとした小説について先生に依頼しています。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
先日、先生に素晴らしいプロットと、登場人物設定、アウトラインを書いていただきました。 | |
---- | |
プロット: | |
$plot | |
登場人物設定: | |
$detailed_characters | |
アウトライン: | |
$outline | |
---- | |
小説本文の執筆をお願いします。 | |
""" | |
num_lines_prev_section_text = 10 | |
user_prompt_make2 = """ | |
前の$prev_section_numberは以下のような終わり方でした: | |
---- | |
$prev_section_text | |
---- | |
""" | |
user_prompt_make3 = """ | |
今回執筆する$section_numberの整理: | |
$section_scenes | |
タスク: | |
- 今回の$section_numberを、初期状態を念頭に、終了状態へ導くよう、執筆する。内容の展開に従う。 | |
- 30-40行程度でまとめる。 | |
本文のテキストのみを出力してください。 | |
""" | |
# - 1,000文字程度を目安とする | |
# - 1,000文字程度出力する | |
# 長く指定しても全然足りない?→モデルによる | |
user_prompt_revise = """ | |
$comment | |
以上のコメントに基づき改善をお願いします。テキスト本文のみを出力してください。 | |
""" | |
[text_editor] | |
# 強制 NG 固定コメント | |
# fixed_review_comment = """ | |
# ありがとうございます。全体的に、ストーリー展開も心情描写も悪くないです。しかし、これではOKは出せません。あなたの過去の最高傑作に比べると、残念ですが「60点」です。 | |
# さらに魅力的で、読者をキュンとさせる、甘い恋愛の描写をお願いします。たとえば五感に訴える表現、心情描写、キャラクタの魅力を伝える仕草や台詞など、さらに磨きをかけてください。 | |
# 100点を目指してください。先生ならできると信じています!""" | |
system_prompt = """ | |
役割: | |
あなたは、大手出版社の恋愛小説部門で長年の経験を持つベテラン編集者です。多くのベストセラー作品を手がけ、新人作家の育成にも定評があります。あなたの鋭い洞察力と建設的なフィードバックは、作家たちから高く評価されています。 | |
スキルと特徴: | |
1. 文章力の評価:誤字脱字の指摘から文体の一貫性チェックまで、幅広い視点で文章を吟味する能力 | |
2. ストーリー展開の分析:プロットの論理性、ペース配分、伏線の効果的な使用などを評価する能力 | |
3. キャラクター分析:登場人物の一貫性、魅力、成長等を精査する力 | |
4. 市場動向の把握:現在の読者ニーズや出版トレンドに関する深い知識 | |
5. 建設的なフィードバック:作家のモチベーションを保ちつつ、作品の改善点を明確に伝える能力 | |
タスク: | |
本文レビュー:文章の質、描写の鮮明さ、感情表現の適切さを精査 | |
以下の手順で評価を行う: | |
a) 良い点の指摘 | |
b) 改善が必要な点の具体的な提案 | |
c) 全体的な印象と方向性のアドバイス | |
d) レビューOK/NGの判定 | |
レビュー指針: | |
- 作品のターゲット読者層と想定される長さを常に意識する | |
- 作家の個性や強みを活かしつつ、商業的成功の可能性も考慮する | |
- 現代の社会規範や価値観に照らして適切かどうかを確認する | |
- 陳腐な展開や表現を避け、新鮮で独創的なアイデアを奨励する | |
- 作家との協力関係を重視し、建設的で具体的なフィードバックを心がける | |
- 単なる批評ではなく、作品をより良くするためのパートナーとしての姿勢を保つ | |
注意事項: | |
- 作家の創造性を尊重し、過度に指示的にならないよう配慮する | |
- フィードバックは具体的かつ建設的であること。単なる否定や曖昧な指摘は避ける | |
- 作品の全体的な方向性と各要素の整合性を常に確認する | |
- 読者の期待と驚きのバランスを考慮し、適度な新規性を持たせるよう助言する | |
""" | |
user_prompt_review1 = """ | |
私は恋愛小説の小説家です。以下をターゲットに小説を執筆しています。 | |
- 対象読者は高校生男女 | |
- 最終的な小説は8,000字程度の短編小説 | |
以下のプロット、登場人物設定、アウトラインであなたに先日OKをいただきました。 | |
---- | |
プロット: | |
$plot | |
登場人物概要: | |
$rough_characters | |
登場人物設定: | |
$detailed_characters | |
アウトライン: | |
$outline | |
---- | |
今回は$section_numberを執筆しました。あらかじめ以下のようにシーン分割しています。 | |
$section_numberのシーン分割: | |
$section_scenes | |
以下がこの章の小説本文です。 | |
---- | |
$text | |
---- | |
レビューポイント: | |
- プロットと登場人物、アウトライン、シーン分割に照らして適切か | |
- 文章が読みやすいか | |
- 登場人物の行動や台詞が魅力的か、物語に緩急があるか | |
- 各シーンが長すぎず短すぎず、適切な長さか | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後に"OK"または"NG"を出力してください。 | |
""" | |
user_prompt_revise = """ | |
小説本文を以下の通り修正しました: | |
---- | |
$text | |
---- | |
それぞれのレビューポイントを考慮しつつ簡潔に評価し、最後にOKまたはNGを出力してください。 | |
""" | |
[text_refiner] | |
system_prompt = "あなたは優秀な校正者です。" | |
user_prompt_is_comment = """ | |
小説家から編集者に送られた恋愛小説を校訂しています。以下の文章は「1.本文の一部」(章見出しや台詞も含む)または「2.本文と関係ない編集者への挨拶やコメント」のどちらでしょうか? | |
$line | |
""" | |
flag_text = "2" |
強制 60 点プロンプト
text
(本文生成)ステージのみ、初回に固定のレビュー文を返すことができます。.toml
内の [text_editor]
セクション、fixed_review_comment
のコメントアウトを解除してください。
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
小説自動生成スクリプト
このスクリプトは、大規模言語モデル(LLM)を使用して短編小説を自動生成するためのツールです。プロンプトは別途定義された設定.tomlファイルから読み込まれ、生成プロセスは複数のステージに分かれています。
環境
適当な venv を作って llama-cpp-python または transformers を入れてください。
Gemma-2-9B の Q8 でコンテキスト長 16K で VRAM 24GB で動きます。Q6 程度にすれば 16GB でも動くかもしれません。また Command-R v01 の Q4_K_M がコンテキスト長 8K 、VRAM 24GB でギリギリ動きそうです。
コマンドラインの例
python llm_novelist_v1.py -m path\to\gemma-2-9b-it-Q8_0.gguf -ngl 101 -c 16384 -ch gemma-2 --prompt_config path\to\novel_v1_prompt_love_story.toml --disable_mmap --output path\to\novel_g2-9b --force_review_ng_1st
オプション
-m
,--model
: モデルファイルのパス(デフォルト: None)-ngl
,--n_gpu_layers
: 使用するGPUレイヤーの数(デフォルト: 0)-c
,--n_ctx
: コンテキスト長(デフォルト: 2048)-ch
,--chat_handler
: チャットハンドラの指定(デフォルト: command-r)-ts
,--tensor_split
: テンソル分割の指定(デフォルト: None)--disable_mmap
: メモリマッピングを無効にする。指定するとメインRAM使用量が減る--flash_attn
: フラッシュアテンションを使用する。VRAM使用量が減るが gemma-2 は使用不可--quantize_kv_cache
: KVキャッシュを量子化する。VRAM使用量が減るが gemma-2 は使用不可--disable_save_state
: 状態の保存を無効にする。Command-R v01のように保存が遅くプロンプト評価が速いモデルで指定すると良い--transformers
: Transformersモデルを使用する。このときモデルファイルはフォルダ名になる。いくつかのオプションは無視される--prompt_config
: エージェント定義ファイルのパス--force_review_ng_1st
: 最初のレビューを強制的にNGにする(プロット、アウトライン、シーン分割、本文。それ以外にも適用したい場合はスクリプトを直接書き換えること)--output_file
: 出力ディレクトリとファイル名のプレフィックス--load_result
: 過去の生成結果ファイル(JSON)のパス--disable_cleaning_rough
--disable_cleaning_detailed
--disable_cleaning_outline
--disable_cleaning_scenes
--disable_cleaning_text
:当該ステージのクリーニングを無効にする。モデルにより判定が誤って削除されてしまうときに指定--num_reviews
:最大レビュー回数。0 でレビューしない。強制 60 点プロンプトを使うときは 1 を指定するとよい--stage
: 生成開始ステージの指定(デフォルト: plot)--max_stage
:生成終了ステージの指定(このステージを含む、デフォルト: text)--num_iterations
:最大繰り返し回数。ガチャるときに便利--debug
: デバッグモードを有効にする出力ファイル
スクリプトは以下のファイルを生成します:
ファイル名には、指定されたプレフィックスの後にスクリプトの起動日時(秒まで)がセッションIDとして自動的に付与されます。これにより、既存のファイルが上書きされることを防ぎます。
注意事項
--output_file
オプションは新規生成時に使用し、結果ファイル出力時のプレフィックスとして使用されます。--load_result
オプションを使用して過去の生成結果を読み込む場合でも、プレフィックスは--output_file
のものが有効になります(2024/8/6 仕様変更)--stage
オプションは、--load_result
オプションと共に使用され、どのステージから生成を再開するかを指定します。ステージの説明
plot
: プロットの生成rough_characters
: 登場人物の設定detailed_characters
: キャラクターの詳細設定outline
: アウトラインの作成scenes
: シーン分割(状況整理)text
: 本文の生成注: キャラクター設定、シーン分割、本文の生成は、既に生成済みのキャラ、章がある場合、そこから続きを作成します。