最近、スマートコントラクトの学習を強化するために dapp の小プロジェクトを作成し、AI を使って NFT のカスタマイズと楽しさを向上させることを試みました。この記事では、その中のいくつかの重要なステップを記録します。
ブレインストーミング#
私はまずアイデアを出し合い、最終的な効果を想像し、そこから逆算して実現方法を考えます。
- AI を使ってアバターを生成しますが、単にアバターを生成するのは少し退屈です。生成されたアバターは現実世界の状態と関連付けられるべきです。私は、AI のブラックボックスの出力と現実世界の状態を固定するためのブロックのようなものを求めています。
- カード抽選要素を追加します。AI による絵画において、この「カード」は「絵画キーワード」として設定されます。
- 生成されたアバターは NFT として鋳造可能ですが、単に鋳造するだけでは少し退屈です。それは単なるチェーン上の画像であるべきではなく、より「正規品」の感覚を持つべきです。私は、正規のゲームやアートにお金を払うのは、作者を支援するためだけでなく、後続のサポートサービスのためでもあります。NFT の自然な取引特性に加えて、鋳造後のユーザーはより多くの追加機能を持つべきです。
最初のバージョンのユーザー動線を簡単に確認します:
- 入ったら、生成ボタンをクリックします(追加の設定が可能です)、アバター画像を生成します。
- 生成後、その中の 1 つを NFT として鋳造することができます。
- 鋳造する場合は、ウォレットを接続し、鋳造をクリックします。
- 鋳造が完了すると、鋳造情報が記録され、後続のサービス提供の基礎となります(鋳造された画像のシードに基づいて再生成することができます)。同時に一定のポイントが報酬として与えられ、絵画キーワードが抽選されます。
効果プレビュー#
GPU サーバーは非常に高価です。悪意のある DDoS 攻撃を防ぐために、オンラインでの試遊は一時的に数人の友人にのみ開放されています。ここでプロセスを確認するためのスクリーンショットを見てみましょう:
初期状態では、ユーザーはウォレットを接続しておらず、「flower」キーワードのみが使用可能です:
「make a wish」をクリックするとアバターが生成されます。
各アバターの結果は、生成された時間や場所などと一定の関連があります。例えば、日本で昼間に生成すると、アバターの人物は昼間に和服を着ているでしょう:
画像を取得した後、鋳造を行いたい場合はウォレットを接続する必要があります:
ウォレットを接続し、鋳造に成功すると、抽選された絵画キーワードが贈られます:
ホームページに戻ると、鋳造された画像に基づいてアバター画像を生成できることがわかり、追加の使用可能なキーワードも増えます:
以前のアバターの基礎の上に新しいキーワードを使って生成してみましょう:
新しいキーワード「Mononoke Hime」は『もののけ姫』に由来し、生成されたアバターももののけ姫のスタイルになります:
プレビューは以上です。次に、アプリケーションの各部分がどのように実現されているかについて説明します。
AI アバター生成サービス#
現在最も人気のあるオープンソースの AI アート生成プロジェクトである stable-diffusion-webui(以下 sd-webui)は、デプロイ後にローカルの Web UI サービスを直接起動することも、API モードで実行して他のサービスから呼び出すためのインターフェースを公開することもできます。起動時に--api
パラメータを追加するだけです。
リポジトリをクローンしてインストールし、sd-webui のリポジトリ内で次のコマンドを実行します。
./webui.sh --api
または
./webui.sh --nowebui
違いは、--api
は API インターフェースサービスを提供するだけでなく、元のローカル Web UI サービスも起動しますが、--nowebui
は単にインターフェースサービスを提供します。
起動に成功したら、次のウェブページを開きます:
http://127.0.0.1:7860/docs
、対応するインターフェースドキュメントが表示されるはずです:
その後、普通のインターフェースを呼び出すように sd-webui のインターフェースを呼び出すことができます。例えば、最も一般的な text2img 機能:
インターフェースから返される画像リソースは base64 文字列で、フロントエンドでの処理が容易なだけでなく、sd モデルのブラックボックスノイズのランダム性を利用して、base64 文字列または画像シードをハッシュ化し、抽選などの他の機能の擬似ランダムシードとして使用できます。
デプロイ後にクロスオリジンリクエストを行う必要がある場合は、"webui.sh" を実行する際に--cors-allow-origins
パラメータを追加します。
※ インストールプロセスに問題が多い場合は、直接この docker イメージを使用してみてください:https://hub.docker.com/r/kestr3l/stable-diffusion-webui
コントラクト作成#
コントラクトに必要な機能:
- 基本的な NFT 機能(鋳造、転送、関連付け可能な tokenURI)。
- 特定のユーザーが所有するすべての NFT tokenId を記録します。
- ポイント機能(後続のサービスで使用されます)。
今回はERC721Enumerable
を基盤としたコントラクトを選択してスマートコントラクトを作成します。コントラクトコードは以下の通りです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";
error Insufficient_Mint_Value();
contract HimeNft is ERC721Enumerable, Ownable {
uint256 private constant MINT_PRICE = 0.1 * 1e18;
uint256 private _tokenId = 0;
mapping(uint256 => string) private _tokenURIs;
mapping(address => uint256) private _points;
constructor() ERC721("HimeNft", "HN") {}
function mintNft(
string calldata tokenURI
) public payable returns (uint256) {
if (msg.value < MINT_PRICE) revert Insufficient_Mint_Value();
_safeMint(msg.sender, _tokenId);
_setTokenURI(_tokenId, tokenURI);
_tokenId += 1;
_points[msg.sender] += 10;
return _tokenId;
}
function withDraw() public onlyOwner {
(bool callSuccess, ) = payable(msg.sender).call{
value: address(this).balance
}("");
require(callSuccess, "call failed");
}
function _setTokenURI(
uint256 tokenId,
string memory _tokenURI
) internal virtual {
require(
_exists(tokenId),
"ERC721URIStorage: URI set of nonexistent token"
);
_tokenURIs[tokenId] = _tokenURI;
}
function tokenURI(
uint256 tokenId
) public view virtual override returns (string memory) {
_requireMinted(tokenId);
return _tokenURIs[tokenId];
}
function getTokenCounter() public view returns (uint256) {
return _tokenId;
}
function getPoints(address owner) public view returns (uint256) {
return _points[owner];
}
function getOwnedTokens(
address owner
) public view returns (uint256[] memory) {
uint256 tokenCount = balanceOf(owner);
uint256[] memory result = new uint256[](tokenCount);
for (uint256 i = 0; i < tokenCount; i++) {
result[i] = tokenOfOwnerByIndex(owner, i);
}
return result;
}
}
※ ここでは簡単に実装していますが、今後の機能アップグレードのために、プロキシコントラクトを使用してアプリケーションとコントラクト間のインタラクションを代理することをお勧めします。
ローカルテストを容易にするためのテストコードも添付します:
const { assert, expect } = require("chai");
const { deployments, ethers, getNamedAccounts } = require("hardhat");
describe("HimeNft", async () => {
const MINT_VALUE = ethers.utils.parseEther("0.1");
const INSUFFICIENT_MINT_VALUE = MINT_VALUE.sub(1);
const TEST_TOKENURI = "TEST_TOKENUR";
let deployer, nft;
beforeEach(async () => {
await deployments.fixture();
deployer = (await getNamedAccounts()).deployer;
nft = await ethers.getContract("HimeNft", deployer);
});
it("初期トークン数は0です", async () => {
const counter = await nft.getTokenCounter();
assert.equal(counter, 0);
});
it("鋳造価値が不十分な場合はリバートします", async () => {
expect(
nft.mintNft(TEST_TOKENURI, {
value: INSUFFICIENT_MINT_VALUE,
})
).to.be.revertedWith("Insufficient_Mint_Value");
});
it("鋳造後に正しいポイントが追加されます", async () => {
await nft.mintNft(TEST_TOKENURI, {
value: MINT_VALUE,
});
const points = (await nft.getPoints(deployer)).toString();
assert.equal(points, "10");
});
it("正しいtokenURIとトークンリストを取得します", async () => {
const TEST_TOKENURI0 = "TEST_TOKENURI0";
const TEST_TOKENURI1 = "TEST_TOKENURI1";
await Promise.all([
nft.mintNft(TEST_TOKENURI0, {
value: MINT_VALUE,
}),
nft.mintNft(TEST_TOKENURI1, {
value: MINT_VALUE,
}),
]);
const list = (await nft.getOwnedTokens(deployer)).toString();
const tokenURI0 = await nft.tokenURI(0);
assert.equal(tokenURI0, TEST_TOKENURI0);
assert.equal(list, "0,1");
});
});
しかし、ここには「絵画キーワードを抽選する」というロジックがないようです。
上記で言及した「画像シードをハッシュ化して、抽選などの他の機能の擬似ランダムシードとして使用する」ということを覚えていますか?ストレージスペースを節約するために、私は画像のシードを NFT のメタデータに保存し、シードを絵画キーワードにマッピングしました。こうすることで、鋳造と同時に「絵画キーワードを抽選する」ことも可能になり、ガスも節約できます。
例えば、最も単純な方法として、次のようなマップを持っています:
{
"royal": 0,
"cute": 1,
}
シードを mod で処理します。シードが 334451 の場合、除数を 10 に設定し、mod (334451,10) を取ります。結果は 1 で、対応する抽選結果は「cute」キーワードになります。もちろん、このマッピングアルゴリズムはさらに複雑にすることもでき、各項目の確率を設定することもできます。
十分な透明性を確保するために、このマップ方法はブロックチェーンにアップロードし、検証するべきです。
コントラクト内でも「_points」というマップが定義されており、ポイントを保存します。このポイントは後で他のサービスのトークンとして使用されます(ここでは簡単のために追加の ERC20 コントラクトを使用してトークンを実装していません。もしそれをより「標準的な」トークンにしたい場合は、ERC20 を個別に実装するか、このコントラクトを ERC1155 に変更して複数のトークン管理を実現できます)。
フロントエンドインターフェースの構築#
今回は next.js+tailwind+react-moralis+ethers.js を使用して、フロントエンドのサービスを構築、デプロイしました。
ここでは詳しく説明しませんが、コードは https://github.com/moayuisuda/HimeAvatar にあります。
特に注意すべきは、react-moralis のuseWeb3Contract
フックが分散呼び出しをサポートしていることです:
コントラクトメソッドを直接呼び出すことができます:
const { runContractFunction } = useWeb3Contract({
abi,
contractAddress: address[chainId as keyof typeof address],
functionName: "mintNft",
params: { tokenURI: "TOKENURL" },
});
runContractFunction()
または、コントラクトの abi、アドレスなどの基本情報を先に定義してから、具体的なメソッドを呼び出すこともできます:
const { runContractFunction } = useWeb3Contract({
abi,
contractAddress: address[chainId as keyof typeof address],
});
runContractFunction({
params: {
functionName: "mintNft",
params: { tokenURI: "TOKENURL" },
},
})
これで各部分の構築が完了しました。これらのサービスを Docker にパッケージ化し、サーバーでワンクリックデプロイを容易にすることができます。
Todo#
- openai の SDK を使用して、一貫した世界観や対話を構築します(個人的には Python バージョンの SDK をお勧めします。Node.js バージョンは使いにくいと感じます)。単なるアバターの構築を「キャラクター」の構築に変えます。
- 制作者が自分でシード画像をアップロードし、lora モデルを生成し、画像を生産し、制作者に分配することをサポートします。
- ポイントトークンの追加サービスストアを構築します。
ギフト#
アプリケーション内のモデルは私が個人的に生成したもので、大モデルのベースモデルには著作権の問題があるかもしれませんので、単にアバター画像を生成することは完全に無料です。
ここに以前生成した良いアバターのいくつかを共有しますので、皆さんが気に入ったら使ってください: