Skip to content

Instantly share code, notes, and snippets.

@kohya-ss
Last active September 5, 2024 07:36
Show Gist options
  • Save kohya-ss/68d41a9720bfbdfd87869ec970142f4b to your computer and use it in GitHub Desktop.
Save kohya-ss/68d41a9720bfbdfd87869ec970142f4b to your computer and use it in GitHub Desktop.
ローカルLLMに小説を書いてもらう
# 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)
# とりあえず恋愛小説用のサンプル:プロンプトを記述してあるだけなので、どこに何を出すか変えるときにはスクリプト側も修正要
[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"
@kohya-ss
Copy link
Author

kohya-ss commented Jul 15, 2024

小説自動生成スクリプト

このスクリプトは、大規模言語モデル(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)
    • 選択肢: command-r, gemma-2 など
  • -ts, --tensor_split : テンソル分割の指定(デフォルト: None)
    • 複数GPU利用時に指定、各GPUに対するカンマ区切りの浮動小数点値
  • --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)
    • 選択肢: plot, rough_characters, detailed_characters, outline, scenes, text
  • --max_stage:生成終了ステージの指定(このステージを含む、デフォルト: text)
  • --num_iterations:最大繰り返し回数。ガチャるときに便利
  • --debug : デバッグモードを有効にする

出力ファイル

スクリプトは以下のファイルを生成します:

  1. LLMへ送信されたプロンプトログ
  2. 各ステージごとの生成物(プロット、登場人物設定など)のJSONファイル
  3. 最終的な小説本文テキストファイル(最後まで生成した場合)

ファイル名には、指定されたプレフィックスの後にスクリプトの起動日時(秒まで)がセッションIDとして自動的に付与されます。これにより、既存のファイルが上書きされることを防ぎます。

注意事項

  • スクリプトが予期せぬ動作をした場合や、Ctrl+Cで中断された場合、そこまでの結果(レビュー完了したもの)がログとJSONに保存されます。
  • --output_fileオプションは新規生成時に使用し、結果ファイル出力時のプレフィックスとして使用されます。
  • --load_resultオプションを使用して過去の生成結果を読み込む場合でも、プレフィックスは --output_file のものが有効になります(2024/8/6 仕様変更)
  • --stageオプションは、--load_resultオプションと共に使用され、どのステージから生成を再開するかを指定します。

ステージの説明

  • plot: プロットの生成
  • rough_characters: 登場人物の設定
  • detailed_characters: キャラクターの詳細設定
  • outline: アウトラインの作成
  • scenes: シーン分割(状況整理)
  • text: 本文の生成

注: キャラクター設定、シーン分割、本文の生成は、既に生成済みのキャラ、章がある場合、そこから続きを作成します。

@kohya-ss
Copy link
Author

kohya-ss commented Aug 6, 2024

強制 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