[크래프톤 정글 3기] 1/27(토) TIL

ClassBinu·2024년 1월 27일
0

크래프톤 정글 3기 TIL

목록 보기
99/120

포진이 더 심해짐😱
수면 시간 조금만 더 늘리기

스토리파이 개발

API 분리

프론트엔드에서 처리하기 편하게 텍스트만 입력하면 스토리 생성 -> 프롬프트 생성 -> 삽화 생성 -> 책 생성을 한 번에 처리하게 했더니, 프론트에서 생성 과정 등을 볼 수 없음.

멘토님과 코치님의 피드백 대로 뭔가 생성되는 과정을 인터랙티브하게 보여주려면 각 단계를 나눠야 함.

이 경우 지금 하나의 함수로 묶어 놓은 API 서비스 로직을 분리 해야 함.

기존의 함수를 다음과 같이 분리함.
1. AI 스토리를 생성하고 클라이언트에 반환하다.
(이 때 텍스트가 동적으로 생성되는 모습을 렌더링한다.)
2. 동시에 AI 스토리를 넘겨셔 책을 생성한다.
2-1. 삽화 프롬프트를 생성한다.
2-2. 삽와 이미지를 생성 후 S3에 저장한다.
2-3. Book 객체를 생성하고 DB에 저장한다.
2-4. Book 객체를 반환한다.

  // 프롬프트를 바탕으로 삽화를 생성하는 함수
  async stableDiffusion(prompt: string): Promise<any> {
    const THEME_LIST = {
      storybook: {
        url: 'https://api-inference.huggingface.co/models/artificialguybr/StoryBookRedmond-V2',
        trigger: 'KidsRedmAF, Kids Book,',
        lora: '<lora:StorybookRedmondV2-KidsBook-KidsRedmAF:1>',
      },
      ghibli: {
        url: 'https://api-inference.huggingface.co/models/artificialguybr/StudioGhibli.Redmond-V2',
        trigger: 'Studio Ghibli, StdGBRedmAF,',
        lora: '<lora:StdGBRedmAF21Config4WithTEV2:1>',
      },
      cartoon: {
        url: 'https://api-inference.huggingface.co/models/artificialguybr/CuteCartoonRedmond-V2',
        trigger: 'CuteCartoonAF, Cute Cartoon,',
        lora: '<lora:CuteCartoonRedmond-CuteCartoon-CuteCartoonAF:1>',
      },
    };

    const theme = 'cartoon';
    const negativePrompt =
      'bad art, ugly, deformed, watermark, duplicated, ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, disfigured, body out of frame, blurry, bad anatomy, blurred, grainy, signature, cut off, draft';

    const API_URL = THEME_LIST[theme].url;
    const TRIGGER_WORDS = THEME_LIST[theme].trigger;
    const LORA = THEME_LIST[theme].lora;
    const HUGGINFACE_API_KEY =
      this.configService.get<string>('HUGGINFACE_API_KEY');

    const headers = {
      Authorization: `Bearer ${HUGGINFACE_API_KEY}`,
    };

    const payload = {
      inputs: `${TRIGGER_WORDS} detailed, best_quality, ${prompt}, ${LORA}`,
      // inputs: prompt,
      parameters: {
        negative_prompt: negativePrompt,
        min_length: false,
        max_length: false,
        top_k: false,
        top_p: false,
        temperature: 0,
        repetition_penalty: false,
        max_time: false,
      },
      options: {
        use_cache: false,
        wait_for_model: false,
      },
    };

    const res = await fetch(API_URL, {
      method: 'POST',
      headers,
      body: JSON.stringify(payload),
    });
    const arrayBuffer = await res.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    return buffer;
  }

  // LLM으로 텍스트를 생성하는 베이스 함수
  async generateAiText(systemMessage: string, userMessage: string) {
    const chatModel = new ChatOpenAI({
      openAIApiKey: this.configService.get<string>('OPENAI_API_KEY'),
      modelName: 'gpt-3.5-turbo-1106',
      // modelName: 'gpt-4-1106-preview',
      temperature: 0.1,
    });

    const prompt = ChatPromptTemplate.fromMessages([
      ['system', systemMessage],
      ['user', '{input}'],
    ]);

    const chain = prompt.pipe(chatModel);
    const res = await chain.invoke({
      input: userMessage,
    });

    return res.content;
  }

  // LLM으로 이야기를 생성하는 함수
  async createAiStory(createAiStoryDto: CreateAiStoryDto, userId: string) {
    const systemMessage = `
    # role
    You are a children's story writer.

    # directive
    Creates a story based on user input.

    # Constraints
    1. In Korean.
    1. '제목: [이야기의 제목]' 형식으로 시작한다.
    1. The story is created with at least four paragraphs separated by blank lines.
    1. Each paragraph must be less than 100 characters.
    1. The story must be created with at least 400 characters.
    `;
    const userMessage = createAiStoryDto.message;
    const createdAiStory = await this.generateAiText(
      systemMessage,
      userMessage,
    );

    const createdStory = await this.storiesService.createStory({
      userId,
      message: createAiStoryDto.message,
    });

    if (!createdStory) {
      throw new Error('Story not created');
    }

    return { content: createdAiStory, story: createdStory };
  }

  // LLM으로 삽화 프롬프트를 생성하는 함수
  async createImagePrompts(storyText: string) {
    const storyArray = storyText.split('\n\n');

    const systemMessage = `
    # directive
    1. In English
    1. Create ${storyArray.length - 1} image prompts about people and landscapes creation to go with this story. 
    1. Each prompt consists of at least 3 words. Like "[lovely_girl, orange_hair, cozy, warm, happy, under_the_tree, sunshie]"
    1. Each prompt is returned in the form of an array, and the array has ${storyArray.length - 1} elements.
    1. Return the prompts as a JSON array, with each prompt consisting of descriptive elements in a sub-array.
    1. People's names are not used and only objective situations are described.
    1. Characters such as must start with '[' and end with ']'.
    `;
    const userMessage = storyText;
    const createdImagePrompts = await this.generateAiText(
      systemMessage,
      userMessage,
    );

    const createdImagePromptsString = createdImagePrompts.toString();
    const startIndex = createdImagePromptsString.indexOf('[');
    const endIndex = createdImagePromptsString.lastIndexOf(']');
    const createdImagePromptsSubstring = createdImagePromptsString.substring(
      startIndex,
      endIndex + 1,
    );

    try {
      const createdImagePromptsArray = JSON.parse(createdImagePromptsSubstring);
      return createdImagePromptsArray;
    } catch (error) {
      console.error('Error parsing JSON:', error);
      return null;
    }
  }

  // AI 책을 생성하는 함수
  async createAiBook(createAiBookDto: CreateAiBookDto, userId: string) {
    const storyText = createAiBookDto.aiStory;
    const storyId = createAiBookDto.storyId;
    const storyArray = storyText.split('\n\n');
    const title = storyArray.shift().replace('제목: ', '').replace(/"/g, '');

    const imagePrompts = await this.createImagePrompts(storyText);
    // 삽화 생성 병렬 요청
    const uploadPromises = imagePrompts.map(
      async (prompt: string, i: number) => {
        const buffer = await this.stableDiffusion(prompt);

        const s3Url = await this.storagesService.bufferUploadToS3(
          `${storyId}-${Date.now()}-${i + 1}.png`,
          buffer,
          'png',
        );

        return s3Url;
      },
    );

    const imageUrlArray = await Promise.all(uploadPromises);

    // 책 데이터 생성
    const bookBody = {};
    imageUrlArray.forEach((url, index) => {
      bookBody[index + 1] = {
        imageUrl: url,
        text: storyArray[index],
        imagePrompt: imagePrompts[index].join(', '),
        ttsUrl: '',
      };
    });

    const createBookDto: CreateBookDto = {
      title,
      coverUrl: bookBody[1].imageUrl,
      body: bookBody,
      storyId: storyId,
      userId: userId,
    };

    console.log(createBookDto);

    // book 데이터 생성 코드 필요
    return await this.booksService.createBook(createBookDto);
  }

삽화 재생성 함수

마음에 안 드는 삽화는 재생성 할 수 있게 한다.
지금은 재생성 시간이 12초 정도 걸리고,
항상 교체되어 버리는 문제가 있지만 우선 최소 기능만 구현하고 추가로 다음 기능 구현할 예정

  1. 기존에 생성한 이미지 불러오는 기능
  2. 미리 여러 장의 이미지를 병렬로 생성해서 일정 수량의 이미지는 빠르게 재생성 가능하게
// 기존 삽화를 재생성하는 함수
  async updateAiBooksImages(id: string, page: string, userId: string) {
    const book = await this.booksService.findBookById(id);
    if (!book) {
      throw new Error('Book not found');
    }

    if (book.userId.toString() !== userId) {
      throw new Error('User not authorized');
    }

    const bookBody = book.body;
    console.log(bookBody.get(page));
    const imagePrompt = bookBody.get(page).imagePrompt;

    const buffer = await this.stableDiffusion(imagePrompt);
    const s3Url = await this.storagesService.bufferUploadToS3(
      `${book.storyId}-${Date.now()}-${page}.png`,
      buffer,
      'png',
    );

    bookBody.get(page).imageUrl = s3Url;

    // 1페이지면 표지도 연동(임시)
    if (page === '1') {
      book.coverUrl = s3Url;
    }

    const updateBookDto: UpdateBookDto = {
      title: book.title,
      coverUrl: book.coverUrl,
      body: Object.fromEntries(bookBody),
    };

    return await this.booksService.updateBook(id, updateBookDto, userId);
  }

이 때 자바스크립트의 map 객체를 이해하지 못해서 map 객체의 프로퍼티에 접근하지 못하는 문제가 있었지만 해결함.

map 객체

자바스크립트에서 Map 객체는 키와 값의 쌍을 저장하는데 사용됩니다. Map은 객체와 달리 키로 어떤 타입의 값이든 사용할 수 있으며, Map의 키는 유일해야 합니다. Map 객체의 주요 특징과 사용 방법은 다음과 같습니다:

주요 특징

키의 다양성: Map의 키는 객체를 포함한 어떤 타입의 값이든 될 수 있습니다.
요소 순서 보존: 삽입된 순서대로 요소가 정렬됩니다. 이는 일반 객체와의 중요한 차이점 중 하나입니다.
크기 속성: Map 객체의 크기를 쉽게 알 수 있는 size 속성이 있습니다.
반복 가능: Map 객체는 for...of 루프나 forEach() 메서드를 사용하여 반복할 수 있습니다.

기본 메서드

new Map(): 새로운 Map 객체를 생성합니다.
map.set(key, value): 키와 값을 Map에 추가합니다.
map.get(key): 키에 해당하는 값을 반환합니다. 키가 존재하지 않으면 undefined를 반환합니다.
map.has(key): Map이 특정 키를 가지고 있는지 여부를 반환합니다 (true 또는 false).
map.delete(key): 키에 해당하는 요소를 Map에서 삭제합니다.
map.clear(): Map의 모든 요소를 제거합니다.
map.size: Map에 있는 요소의 개수를 반환합니다.

예시

let map = new Map();

map.set('name', 'Alice');
map.set(1, 'one');
map.set(true, 'bool');

console.log(map.get('name')); // 'Alice'
console.log(map.size); // 3

for (let [key, value] of map) {
  console.log(key, value);
}

위 예시에서 보듯이, Map은 다양한 타입의 키를 사용할 수 있고, 삽입된 순서대로 요소를 반복할 수 있습니다. 이러한 특징으로 인해 Map은 복잡한 데이터 구조를 필요로 하는 경우에 유용하게 사용될 수 있습니다.

0개의 댓글