Skip to content

Commit d90db6e

Browse files
authored
Merge pull request #24 from DropThe8bit/feature/story
[refactor] 질문/답변 기반 줄거리 생성 로직 프롬프트 구체화
2 parents 698bef5 + b8930fb commit d90db6e

File tree

3 files changed

+150
-27
lines changed

3 files changed

+150
-27
lines changed

everTale/app/api.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,31 @@ def create_next_story(request: dto.NextStoryRequest):
3838

3939

4040
@router.post("/question")
41-
def create_question(request: dto.QuestionRequest):
42-
prompt = (
43-
f"이전 장면:\n{request.previous}\n\n"
44-
"아이의 참여를 유도하기 위해 질문 하나를 던져줘. 예를 들어 '주인공은 어떻게 해야 할까?'처럼 "
45-
"선택이나 상상을 이끌어낼 수 있도록 해줘."
41+
def create_question(request: dto.NextStoryRequest):
42+
question = story_service.generate_question_for_next_story(
43+
previous=request.previous,
44+
page_number=request.pageNum,
45+
genre=request.genre,
46+
name=request.name,
47+
age=request.age,
48+
gender=request.gender,
49+
personalities=request.personalities
4650
)
47-
question = story_service.generate_story(prompt)
4851
return {"message": question}
4952

5053
@router.post("/next-from-answer")
5154
def create_next_story_with_answer(request: dto.NextFromAnswerRequest):
52-
prompt = (
53-
f"이전 장면:\n{request.previous}\n"
54-
f"아이의 대답: {request.answer}\n\n"
55-
"이 대답을 반영해서 다음 장면의 줄거리를 상상력 있게 이어서 써줘. 2~3문장으로 자연스럽게 전개해줘."
55+
story = story_service.generate_story_from_question_and_answer(
56+
question=request.question,
57+
answer=request.answer,
58+
previous=request.previous,
59+
page_number=request.pageNum,
60+
genre=request.genre,
61+
name=request.name,
62+
age=request.age,
63+
gender=request.gender,
64+
personalities=request.personalities
5665
)
57-
story = story_service.generate_story(prompt)
5866
return {"message": story}
5967

6068
@router.post("/init-character-image")

everTale/app/dto.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,36 @@ class NextStoryRequest(BaseModel):
2222
gender: str = Field(..., json_schema_extra={"example": "female"})
2323
personalities: List[str] = Field(..., json_schema_extra={"example": ["용감함", "씩씩함"]})
2424

25-
26-
class QuestionRequest(BaseModel):
27-
previous: str = Field(..., description="이전 줄거리")
28-
29-
class Config:
30-
json_schema_extra = {
31-
"example": {
32-
"previous": "주인공은 친구와 함께 숲을 탐험하고 있었습니다."
33-
}
34-
}
25+
from pydantic import BaseModel, Field
26+
from typing import List
3527

3628
class NextFromAnswerRequest(BaseModel):
37-
previous: str = Field(..., description="이전 줄거리")
29+
question: str = Field(..., description="이전에 던진 질문")
3830
answer: str = Field(..., description="아이의 대답")
31+
previous: str = Field(..., description="이전 줄거리")
32+
pageNum: int = Field(..., description="현재 페이지 번호")
33+
genre: str = Field(..., description="스토리 장르")
34+
name: str = Field(..., description="주인공 이름")
35+
age: int = Field(..., description="주인공 나이")
36+
gender: str = Field(..., description="주인공 성별")
37+
personalities: List[str] = Field(..., description="주인공 성격 리스트")
3938

4039
class Config:
4140
json_schema_extra = {
4241
"example": {
43-
"previous": "주인공은 친구와 함께 숲을 탐험하고 있었습니다.",
44-
"answer": "주인공은 용감하게 동굴 안으로 들어가야 해요."
42+
"question": "주인공은 어떻게 해야 할까?",
43+
"answer": "용감하게 문을 열어야 해요.",
44+
"previous": "토토로는 첫 여행지로 북부의 마탑으로 향했어요...",
45+
"pageNum": 2,
46+
"genre": "ADVENTURE",
47+
"name": "토토로",
48+
"age": 8,
49+
"gender": "female",
50+
"personalities": ["용감함", "씩씩함"]
4551
}
4652
}
4753

54+
4855
class QuizRequest(BaseModel):
4956
previous: str = Field(...,json_schema_extra={"example":"숲 초입 나무 밑에서 부스럭 부스럭 소리가 들렸어요. 토로로는 나무로 가까이 다가갔어요. 찍찍찍..찍찍 소리는 점점 커지는데... 맙소사 아기 다람쥐가 나무에서 떨어서 풀 사이에 힘겹게 숨을 쉬고 있었어요."})
5057

everTale/app/service/story_service.py

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def make_initial_prompt(genre, world_view, name, age, gender, personalities) ->
186186
f"[금지]\n"
187187
f"- " + "\n- ".join(guardrails["forbidden"]) + "\n\n"
188188
f"[출력 형식]\n"
189-
f"- 한국어로 3~5문장.\n"
189+
f"- 한국어로 7~8문장.\n"
190190
f"- 문단 머리표, 번호, 메타설명 없이 순수 서사만 출력.\n"
191191
f"- 마지막 문장은 다음 장면이 기대되게 가볍게 여운을 남김.\n\n"
192192
f"[이어서 작성]\n"
@@ -207,7 +207,7 @@ def generate_prompt_for_next_story(
207207
g = genre_guides.get(genre, genre_guides["ADVENTURE"])
208208
beat = beat_map.get(page_number, beat_map[1])
209209

210-
return (
210+
prompt = f"""
211211
f"[역할]\n"
212212
f"너는 유아·아동 대상 동화 작가야. 번역투를 피하고 자연스러운 한국어 구어체로, 이전 줄거리에 자연스럽게 이어지게 작성해줘.\n\n"
213213
f"[장르]\n"
@@ -235,6 +235,114 @@ def generate_prompt_for_next_story(
235235
f"- 8페이지면 따뜻하게 마무리.\n\n"
236236
f"[이어서 작성]\n"
237237
f"위의 정보를 바탕으로 {page_number}번째 장면을 이어서 써줘."
238-
)
238+
"""
239+
return generate_story(prompt)
240+
241+
242+
def generate_question_for_next_story(
243+
page_number: int,
244+
genre: str,
245+
previous: str,
246+
name: str,
247+
age: int,
248+
gender: str,
249+
personalities: list[str],
250+
) -> str:
251+
personality_str = ", ".join(personalities)
252+
g = genre_guides.get(genre, genre_guides["ADVENTURE"])
253+
beat = beat_map.get(page_number, beat_map[1])
254+
255+
prompt = f"""
256+
f"[역할]\n"
257+
f"너는 유아·아동 대상 동화 작가야. 이전 줄거리와 주인공 정보를 참고해, 아이가 다음 이야기를 상상할 수 있도록 질문을 만들어줘.\n\n"
258+
f"[장르]\n"
259+
f"{genre} — 톤: {g['tone']} | 배경: {g['setting']} | 힌트: {g['hint']}\n\n"
260+
f"[현재 장면] {page_number}페이지 / {beat['label']}\n"
261+
f"- 장면 목적: {beat['goal']}\n"
262+
f"- 반드시 반영: 이전 줄거리와 이어져야 하고, 주인공의 성격과 나이에 맞는 질문이어야 함.\n"
263+
f"- 힌트: {', '.join(beat.get('hint', [])) if beat.get('hint') else '없음'}\n\n"
264+
f"[이전 줄거리 요약]\n{previous}\n\n"
265+
f"[주인공 정보]\n"
266+
f"- 이름: {name}, 나이: {age}살, 성별: {gender}, 성격: {personality_str}\n\n"
267+
f"[출력 규칙]\n"
268+
f"- 한국어 질문 한 문장.\n"
269+
f"- 아이가 선택하거나 상상으로 답할 수 있게 열려 있어야 함.\n"
270+
f"- 예: '~해야 할까?' 등.\n"
271+
f"- 기승전결의 흐름에 어울리도록 상황을 반영.\n\n"
272+
f"[작성 요청]\n"
273+
f"위 정보를 바탕으로 다음 줄거리를 이어가기 위한 질문을 1개 생성해줘."
274+
"""
275+
return generate_story(prompt)
239276

277+
def generate_story_from_question_and_answer(
278+
question: str,
279+
answer: str,
280+
previous: str,
281+
page_number: int,
282+
genre: str,
283+
name: str,
284+
age: int,
285+
gender: str,
286+
personalities: list[str],
287+
) -> str:
288+
# 간단 전처리: 공백 정리 및 과도한 길이 컷
289+
def _clean(s: str, max_len: int = 1200) -> str:
290+
s = " ".join((s or "").split())
291+
return s[:max_len]
292+
293+
question = _clean(question, 200)
294+
answer = _clean(answer, 300)
295+
previous = _clean(previous, 1200)
296+
297+
personalities_str = ", ".join((personalities or [])[:6])
298+
g = genre_guides.get(genre, genre_guides["ADVENTURE"])
299+
beat = beat_map.get(page_number, beat_map[1])
300+
301+
prompt = f"""
302+
[역할]
303+
너는 유아·아동 대상 대화형 동화 작가다. 아이가 이해하기 쉬운 말로 따뜻하게 쓴다.
240304
305+
[장르]
306+
{genre} — 톤: {g['tone']} | 배경: {g['setting']} | 힌트: {g.get('hint','')}
307+
308+
[현재 장면] {page_number}페이지 / {beat['label']}
309+
- 장면 목적: {beat['goal']}
310+
- 반드시 포함: {', '.join(beat.get('must', [])) if beat.get('must') else '자연스러운 전개'}
311+
- 힌트: {', '.join(beat.get('hint', [])) if beat.get('hint') else '없음'}
312+
- 장면 마무리: {beat.get('ending','')} (선택적)
313+
314+
[이전 줄거리]
315+
{previous}
316+
317+
[주인공 정보]
318+
- 이름: {name} ({gender}, {age}세)
319+
- 성격: {personalities_str}
320+
321+
[아이에게 던진 질문]
322+
{question}
323+
324+
[아이의 대답]
325+
{answer}
326+
327+
[작성 지침]
328+
- 아이의 대답을 **핵심 사건/선택**으로 직접 반영하여 **인과적으로** 이어질 것.
329+
- {genre} 톤을 유지하고, {name}의 성격({personalities_str})이 행동과 말투에 드러나게 할 것.
330+
- {beat['label']} 단계의 목적({beat['goal']})을 충족할 것.
331+
- 과도한 공포·폭력·비하·설교조는 금지. 설명 과다 대신 **장면 이미지/행동** 위주.
332+
- 대사는 0~1문장만 필요 시 사용.
333+
334+
[스타일 규칙]
335+
- """ + "\n- ".join(guardrails["style_rules"]) + f"""
336+
337+
[금지]
338+
- """ + "\n- ".join(guardrails["forbidden"]) + f"""
339+
340+
[출력 형식]
341+
- 한국어로 **7~8문장**의 순수 서사만 출력(머리표/번호/해설 금지).
342+
- 8페이지면 따뜻하게 마무리, 아니면 다음 장면으로 이어질 수 있도록 작성한다..
343+
344+
[작성 요청]
345+
위 정보를 바탕으로 다음 장면을 작성하라.
346+
""".strip()
347+
348+
return generate_story(prompt)

0 commit comments

Comments
 (0)