이 튜토리얼에서는 다음 방법을 배우게 됩니다:

  1. Stability AI로 이미지 생성하기
  2. Pinata IPFS에 이미지 업로드하기
  3. Story에 이미지를 IP로 등록하기
  4. IP에 라이선스 조건 첨부하기

설명

Stability AI를 사용하여 이미지를 생성했다고 가정해 봅시다. 이미지에 적절한 라이선스를 추가하지 않으면 다른 사람들이 자유롭게 사용할 수 있습니다. 이 튜토리얼에서는 Stability AI로 생성한 이미지에 라이선스를 추가하여 다른 사람들이 사용하려면 반드시 귀하로부터 적절한 라이선스를 받아야 하도록 하는 방법을 배우게 됩니다.

0. 시작하기 전에

튜토리얼을 시작하기 전에 완료해야 할 몇 가지 단계가 있습니다.

  1. 다음을 설치해야 합니다 Node.jsnpm. 이전에 코딩을 해보셨다면 이미 설치되어 있을 것입니다.
  2. Story Network Testnet 지갑의 개인 키를 .env file:
.env
WALLET_PRIVATE_KEY=
  1. 다음으로 이동하세요 Pinata 대시보드 새로운 API 키와 게이트웨이를 생성하세요. JWT와 게이트웨이를 .env file:
.env
PINATA_JWT=
PINATA_GATEWAY=
  1. 다음으로 이동하세요 Stability 새로운 API 키를 생성하세요. 새 키를 .env file:

Stability 크레딧

이미지를 생성하려면 Stability 크레딧이 필요합니다. 방금 계정을 만들었다면 시작할 수 있는 몇 가지 크레딧이 포함된 무료 평가판이 있을 것입니다.

.env
STABILITY_API_KEY=
  1. 선호하는 RPC URL을 .env 파일에 추가하세요. 우리가 제공하는 기본 공개 URL을 사용할 수 있습니다:
.env
RPC_PROVIDER_URL=https://aeneid.storyrpc.io
  1. 의존성을 설치하세요:
Terminal
npm install @story-protocol/core-sdk pinata-web3 viem axios sharp form-data

1. 이미지 생성하기

다음을 참조하세요 Stability API 참조 원하는 모델을 사용하기 위해. 이 튜토리얼에서는 Stability의 Stable Image Core 생성 엔드포인트를 사용하여 이미지를 생성할 것입니다. 아래 내용은 그들의 문서에서 직접 가져온 것입니다.

파일을 생성하고 main.ts 다음 코드를 추가하세요:

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";

async function main() {
  const payload = {
    prompt: "Lighthouse on a cliff overlooking the ocean",
    output_format: "png",
  };

  const response = await axios.postForm(
    `https://api.stability.ai/v2beta/stable-image/generate/core`,
    axios.toFormData(payload, new FormData()),
    {
      validateStatus: undefined,
      responseType: "arraybuffer",
      headers: {
        Authorization: `Bearer ${process.env.STABILITY_API_KEY}`,
        Accept: "image/*",
      },
    }
  );
}

main();

1.5. (선택사항) 이미지 압축하기

Stability는 크기가 큰 이미지를 생성하므로 저장 비용이 많이 듭니다. 선택적으로 생성된 이미지를 압축하여 더 빠른 로딩 속도와 더 저렴한 저장 비용을 얻을 수 있습니다.

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";

async function main() {
  // previous code here ...

  const condensedImgBuffer = await sharp(response.data)
    .png({ quality: 10 }) // Adjust the quality value as needed (between 0 and 100)
    .toBuffer();
}

main();

2. IPFS에 이미지 저장하기

이제 이미지가 생성되었으니 IPFS에 저장하여 접근할 수 있는 URL을 받아야 합니다. 이 튜토리얼에서는 Pinata를 사용할 것입니다. 이는 이미지 저장을 쉽게 만들어주는 분산 저장 솔루션입니다.

별도의 파일 uploadToIpfs.ts에서 함수를 생성하세요 uploadBlobToIPFS 이 함수는 우리의 버퍼를 IPFS에 업로드합니다:

uploadToIpfs.ts
import { PinataSDK } from "pinata-web3";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT,
  // you can put your pinata gateway here, or leave it empty
  pinataGateway: process.env.PINATA_GATEWAY,
});

// upload our image to ipfs by putting it in a public group
export async function uploadBlobToIPFS(
  blob: Blob,
  fileName: string
): Promise<string> {
  const file = new File([blob], fileName, { type: "image/png" });
  const { IpfsHash } = await pinata.upload.file(file);
  return IpfsHash;
}

메인 파일로 돌아가서 uploadBlobToIPFS 함수를 호출하여 이미지를 저장하세요:

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS } from "./uploadToIpfs.ts";

async function main() {
  // previous code here ...

  // convert the buffer to a blob
  const blob = new Blob([condensedImgBuffer], { type: "image/png" });
  // store the blob on ipfs
  const imageCid = await uploadBlobToIPFS(blob, "lighthouse.png");
}

main();

3. Story 설정하기

이제 이미지를 생성하고 저장했으니 Story에 IP로 등록할 수 있습니다. 먼저 설정을 해봅시다. utils.ts 파일에 다음 코드를 추가하세요:

관련 문서: TypeScript SDK 설정

utils.ts
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";
import { http } from "viem";
import { privateKeyToAccount, Address, Account } from "viem/accounts";

const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`;
export const account: Account = privateKeyToAccount(privateKey);

const config: StoryConfig = {
  account: account,
  transport: http(process.env.RPC_PROVIDER_URL),
  chainId: "aeneid",
};
export const client = StoryClient.newClient(config);

4. IP 메타데이터 설정하기

다음을 참조하세요 IPA 메타데이터 표준 IP의 메타데이터를 구성하세요. 아래와 같이 메타데이터를 적절히 포맷하세요:

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";

async function main() {
  // previous code here ...

  const ipMetadata = {
    title: "Lighthouse",
    description: "A generated picture of a lighthouse.",
    createdAt: "1728401700",
    image: process.env.PINATA_GATEWAY + "/files/" + imageCid,
    imageHash: "0x...", // a hash of the image
    mediaUrl: process.env.PINATA_GATEWAY + "/files/" + imageCid,
    mediaHash: "0x...", // a hash of the image
    mediaType: "image/png",
    creators: [
      {
        name: "Jacob Tucker",
        address: "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084",
        description: "A cool dev rel person.",
        contributionPercent: 100,
        socialMedia: [
          {
            platform: "Twitter",
            url: "https://x.com/jacobmtucker",
          },
        ],
      },
    ],
  };
}

main();

5. NFT 메타데이터 설정하기

NFT 메타데이터는 다음을 따릅니다 ERC-721 메타데이터 표준.

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";

async function main() {
  // previous code here ...

  const nftMetadata = {
    name: "Ownership NFT",
    description:
      "This NFT represents ownership of the image generated by Stability",
    image: process.env.PINATA_GATEWAY + "/files/" + imageCid,
    attributes: [
      {
        key: "Model",
        value: "Stability",
      },
      {
        key: "Service",
        value: "Stable Image Core",
      },
      {
        key: "Prompt",
        value: "Lighthouse on a cliff overlooking the ocean",
      },
    ],
  };
}

main();

6. IP와 NFT 메타데이터를 IPFS에 업로드하기

파일에서 uploadToIpfs.ts IP와 NFT 메타데이터 객체를 IPFS에 업로드하는 함수를 만드세요:

uploadToIpfs.ts
// previous code here ...

export async function uploadJSONToIPFS(jsonMetadata: any): Promise<string> {
  const { IpfsHash } = await pinata.upload.json(jsonMetadata);
  return IpfsHash;
}

그런 다음 아래와 같이 해당 함수를 사용하여 메타데이터를 업로드할 수 있습니다:

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS, uploadJSONToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";
import { createHash } from "crypto";

async function main() {
  // previous code here ...

  const ipIpfsHash = await uploadJSONToIPFS(ipMetadata);
  const ipHash = createHash("sha256")
    .update(JSON.stringify(ipMetadata))
    .digest("hex");
  const nftIpfsHash = await uploadJSONToIPFS(nftMetadata);
  const nftHash = createHash("sha256")
    .update(JSON.stringify(nftMetadata))
    .digest("hex");
}

main();

7. 라이선스 조건 생성하기

Story에 이미지를 등록할 때 라이선스 조건을 IP에 첨부할 수 있습니다. 이는 실제로 법적 구속력이 있는 조건으로, 라이선싱 모듈에 의해 온체인에서 강제되며, 분쟁 모듈에 의해 이의 제기가 가능하며, 최악의 경우 전통적인 방식으로 법정에서 오프체인으로 집행될 수 있습니다.

우리가 만든 이미지를 수익화하고 싶다고 가정해 봅시다. 누군가 이 이미지를 사용하고 싶을 때마다(상품, 광고 등에) 10 $WIP의 초기 발행 수수료를 지불해야 합니다. 또한 파생 작품으로 수익을 얻을 때마다 5%의 수익을 로열티로 지불해야 합니다.

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS, uploadJSONToIPFS } from "./uploadToIpfs.ts";
import { client, account } from "./utils";
import { createHash } from "crypto";
import { LicenseTerms, WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
import { zeroAddress, parseEther } from "viem";

async function main() {
  // previous code here ...

  const commercialRemixTerms: LicenseTerms = {
    transferable: true,
    royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", // RoyaltyPolicyLAP address from https://docs.story.foundation/developers/deployed-smart-contracts
    defaultMintingFee: parseEther("1"), // 1 $WIP
    expiration: BigInt(0),
    commercialUse: true,
    commercialAttribution: true, // must give us attribution
    commercializerChecker: zeroAddress,
    commercializerCheckerData: zeroAddress,
    commercialRevShare: 5, // can claim 50% of derivative revenue
    commercialRevCeiling: BigInt(0),
    derivativesAllowed: true,
    derivativesAttribution: true,
    derivativesApproval: false,
    derivativesReciprocal: true,
    derivativeRevCeiling: BigInt(0),
    currency: WIP_TOKEN_ADDRESS,
    uri: "",
  };
}

main();

8. NFT를 IP Asset으로 등록하기

다음으로 NFT를 발행하고, 이를 IP Asset으로 등록하고, License Terms를 IP에 설정한 다음, NFT와 IP 메타데이터를 모두 설정할 것입니다.

다행히도, 우리는 mintAndRegisterIp 함수를 사용하여 NFT를 발행하고 동시에 IP Asset으로 등록할 수 있습니다.

이 함수는 발행할 SPG NFT 컨트랙트가 필요합니다. 간단히 하기 위해, Aeneid 테스트넷에서 우리가 여러분을 위해 만든 공개 컬렉션을 사용할 수 있습니다: 0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc.

main.ts
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import { uploadBlobToIPFS, uploadJSONToIPFS } from "./uploadToIpfs.ts";
import { WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk";
import { client, account } from "./utils";
import { createHash } from "crypto";
import { LicenseTerms } from "@story-protocol/core-sdk";
import { zeroAddress, parseEther, Address } from "viem";

async function main() {
  // previous code here ...

  const response = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({
    spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc",
    // the terms we created in the previous step
    licenseTermsData: [{ terms: commercialRemixTerms }],
    ipMetadata: {
      ipMetadataURI: process.env.PINATA_GATEWAY + "/files/" + ipIpfsHash,
      ipMetadataHash: `0x${ipHash}`,
      nftMetadataURI: process.env.PINATA_GATEWAY + "/files/" + nftIpfsHash,
      nftMetadataHash: `0x${nftHash}`,
    },
    txOptions: { waitForTransaction: true },
  });

  console.log(
    `Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}`
  );
  console.log(
    `View on the explorer: https://aeneid.explorer.story.foundation/ipa/${response.ipId}`
  );
}

main();

9. 완료!

축하합니다! 이제 여러분의 이미지가 상업적 라이선스 조건과 함께 Story에 등록되었습니다.