@@ -242,4 +263,21 @@ const svgArts = computedAsync(async () => {
max-height: 10lh; /* Adjust this value to control the number of visible lines */
overflow: hidden;
}
+
+/**
+https://stackoverflow.com/questions/67853607/how-do-i-graduallyfeather-gradient-transition-blur-in-css
+https://www.reddit.com/r/css/comments/t3am53/pure_css_gradientprogressive_blur_i_used/
+https://codepen.io/Francesco_Maretti/pen/yLPRvXp
+*/
+.preview-card-art-image-overlay {
+ height: 50%;
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ z-index: 1;
+ transform: translateY(50%);
+ backdrop-filter: blur(40px);
+ -webkit-mask-image: linear-gradient(to bottom, #ffffff00 0%, #ffffff 50%, #ffffff00 100%);
+ mask-image: linear-gradient(to bottom, #ffffff00 0%, #ffffff 50%, #ffffff00 100%);
+}
diff --git a/docs/.vitepress/components/DropdownMenu.vue b/docs/.vitepress/components/DropdownMenu.vue
index e4e19869d..0a1d620f0 100644
--- a/docs/.vitepress/components/DropdownMenu.vue
+++ b/docs/.vitepress/components/DropdownMenu.vue
@@ -39,7 +39,7 @@ defineProps<{
:exit="{ opacity: 0, y: -10 }"
class="z-20 border rounded-xl p-2 shadow-md backdrop-blur-md"
:class="[
- 'bg-white/20 dark:border-white/5 dark:bg-black/20',
+ 'bg-white/80 dark:border-white/5 dark:bg-black/80',
'transition-colors duration-200 ease-in-out',
]"
>
diff --git a/docs/.vitepress/components/Navbar.vue b/docs/.vitepress/components/Navbar.vue
index 39a0c79f9..cc3713f32 100644
--- a/docs/.vitepress/components/Navbar.vue
+++ b/docs/.vitepress/components/Navbar.vue
@@ -99,7 +99,7 @@ function isNavLinkActive(link: string, path: string) {
align="end"
class="will-change-[transform,opacity] z-10 w-[180px] border rounded-xl p-2 shadow-md backdrop-blur-md data-[state=open]:data-[side=bottom]:animate-slideUpAndFade"
:class="[
- 'bg-white/20 dark:border-white/5 dark:bg-black/20',
+ 'bg-white/70 dark:border-white/5 dark:bg-black/70',
]"
transition="colors duration-200 ease-in-out"
>
diff --git a/docs/.vitepress/composables/outline.ts b/docs/.vitepress/composables/outline.ts
index 0f6e951c2..e3153afe3 100644
--- a/docs/.vitepress/composables/outline.ts
+++ b/docs/.vitepress/composables/outline.ts
@@ -1,9 +1,10 @@
import type { DefaultTheme } from 'vitepress/theme'
import type { Ref } from 'vue'
+import { useEventListener } from '@vueuse/core'
// Copied from https://github.com/vuejs/vitepress/blob/97f9469b6d4eb7ba9de9a1111986581d1f704ec3/src/client/theme-default/composables/outline.ts#L4
import { getScrollOffset } from 'vitepress'
-import { onMounted, onUnmounted, onUpdated } from 'vue'
+import { onMounted, onUpdated } from 'vue'
export interface Header {
/**
@@ -153,7 +154,6 @@ export function useActiveAnchor(
onMounted(() => {
requestAnimationFrame(setActiveLink)
- window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
@@ -161,9 +161,7 @@ export function useActiveAnchor(
activateLink(location.hash)
})
- onUnmounted(() => {
- window.removeEventListener('scroll', onScroll)
- })
+ useEventListener('scroll', onScroll)
function setActiveLink() {
const scrollY = window.scrollY
diff --git a/docs/.vitepress/composables/sidebar.ts b/docs/.vitepress/composables/sidebar.ts
index bcd467ee1..ee43638e4 100644
--- a/docs/.vitepress/composables/sidebar.ts
+++ b/docs/.vitepress/composables/sidebar.ts
@@ -1,13 +1,12 @@
import type { DefaultTheme } from 'vitepress/theme'
import type { ComputedRef, Ref } from 'vue'
-import { useMediaQuery } from '@vueuse/core'
+import { useEventListener, useMediaQuery } from '@vueuse/core'
import { useData, withBase } from 'vitepress'
import {
computed,
onMounted,
- onUnmounted,
ref,
watch,
@@ -90,13 +89,7 @@ export function useCloseSidebarOnEscape(
: undefined
})
- onMounted(() => {
- window.addEventListener('keyup', onEscape)
- })
-
- onUnmounted(() => {
- window.removeEventListener('keyup', onEscape)
- })
+ useEventListener('keyup', onEscape)
function onEscape(e: KeyboardEvent) {
if (e.key === 'Escape' && isOpen.value) {
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 093ea80ff..1eb3dc37f 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -24,6 +24,7 @@ import {
rekaShortName,
releases,
} from './meta'
+import { frontmatterAssets } from './plugins/vite-frontmatter-assets'
function withBase(url: string) {
return env.BASE_URL
@@ -87,7 +88,7 @@ export default defineConfig({
outline: {
level: 'deep',
},
- logo: '/favicon.svg',
+ logo: withBase('/favicon.svg'),
sidebar: [
{
@@ -153,7 +154,7 @@ export default defineConfig({
outline: {
level: 'deep',
},
- logo: '/favicon.svg',
+ logo: withBase('/favicon.svg'),
sidebar: [
{
@@ -276,6 +277,7 @@ export default defineConfig({
i18n({ runtimeOnly: true, compositionOnly: true, fullInstall: true, ssr: true }),
unocss(),
yaml(),
+ frontmatterAssets(),
],
css: {
postcss: {
diff --git a/docs/.vitepress/functions/blog.data.ts b/docs/.vitepress/functions/blog.data.ts
index a0c7e263a..8df59785f 100644
--- a/docs/.vitepress/functions/blog.data.ts
+++ b/docs/.vitepress/functions/blog.data.ts
@@ -1,10 +1,17 @@
import type { SiteConfig } from 'vitepress'
+import { createHash } from 'node:crypto'
+import { readFile } from 'node:fs/promises'
+import { parse } from 'node:path'
+import { env } from 'node:process'
+
+import { dirname, join } from 'pathe'
import { createContentLoader } from 'vitepress'
import { formatDate } from './utils'
const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG
+const base = config.userConfig.base || env.BASE_URL || '/'
interface Post {
title: string
@@ -19,13 +26,63 @@ interface Post {
declare const data: Post[]
export { data }
+function cwdFromUrl(url: string): string {
+ if (url.endsWith('/')) {
+ return url
+ }
+
+ return dirname(url)
+}
+
+function fromAtAssets(url: string): string {
+ const reg = /^@assets\(('\S+')|("\S+")|(\S+)\)$/
+ if (reg.test(url)) {
+ const res = url
+ .replace(reg, '$1')
+ .replace(/^\(/, '')
+ .replace(/\)$/, '')
+ .replace(/^'/, '')
+ .replace(/'$/, '')
+ .replace(/^"/, '')
+ .replace(/"$/, '')
+
+ return res
+ }
+
+ return url
+}
+
+function withBase(url?: string, base?: string) {
+ if (!url) {
+ return url
+ }
+
+ if (url.startsWith('/') && base) {
+ return join(base, url)
+ }
+
+ return url
+}
+
+function withDirname(url?: string, cwd?: string) {
+ if (!url || !cwd) {
+ return url
+ }
+
+ if (url.startsWith('/')) {
+ return join(cwd, url)
+ }
+
+ return join(cwd, url)
+}
+
export default createContentLoader('**/blog/**/*.md', {
includeSrc: true,
render: true,
excerpt: true,
- transform(raw): Post[] {
- return raw
- .map(({ url, frontmatter, excerpt }) => {
+ async transform(raw): Promise
{
+ return (await Promise.all(raw
+ .map(async ({ url, frontmatter, excerpt }) => {
const foundLanguage = Object.values(config.userConfig.locales!).find((locale) => {
let normalizedLanguagePrefix = locale.lang || 'en'
if (!normalizedLanguagePrefix.startsWith('/')) {
@@ -35,16 +92,42 @@ export default createContentLoader('**/blog/**/*.md', {
return url.startsWith(normalizedLanguagePrefix)
})
- return {
+ async function fileToUrl(file: string | undefined) {
+ if (config.vite?.build)
+ return file
+ if (!file)
+ return file
+
+ const parsed = parse(file)
+ const hash = createHash('sha256')
+ .update(await readFile(join(config.srcDir, file)))
+ .digest('hex')
+ .slice(0, 8)
+
+ return `/assets/${parsed.name}.${hash}${parsed.ext}`
+ }
+
+ const previewCoverLight = withBase(await fileToUrl(withDirname(fromAtAssets(frontmatter['preview-cover']?.light), cwdFromUrl(url))), base)
+ const previewCoverDark = withBase(await fileToUrl(withDirname(fromAtAssets(frontmatter['preview-cover']?.dark), cwdFromUrl(url))), base)
+
+ const res = {
title: frontmatter.title,
url,
urlWithoutLang: url.replace(`/${foundLanguage?.lang || 'en'}`, ''),
excerpt,
date: formatDate(frontmatter.date),
lang: foundLanguage?.lang || 'en',
- frontmatter,
+ frontmatter: {
+ ...frontmatter,
+ 'preview-cover': {
+ light: previewCoverLight,
+ dark: previewCoverDark,
+ },
+ },
}
- })
+
+ return res
+ })))
.sort((a, b) => b.date.time - a.date.time)
},
})
diff --git a/docs/.vitepress/plugins/vite-frontmatter-assets.ts b/docs/.vitepress/plugins/vite-frontmatter-assets.ts
new file mode 100644
index 000000000..bb68f3e0b
--- /dev/null
+++ b/docs/.vitepress/plugins/vite-frontmatter-assets.ts
@@ -0,0 +1,176 @@
+import type { Plugin, ResolvedConfig } from 'vite'
+import type { SiteConfig } from 'vitepress'
+
+import { createHash } from 'node:crypto'
+import { readFile } from 'node:fs/promises'
+import { dirname, join, parse } from 'node:path'
+
+import matter from 'gray-matter'
+
+import { glob } from 'tinyglobby'
+
+function fromAtAssets(url: string): string {
+ const reg = /^@assets\(('\S+')|("\S+")|(\S+)\)$/
+ if (reg.test(url)) {
+ const res = url
+ .trim()
+ .replace(reg, '$1')
+ .replace(/^\(/, '')
+ .replace(/\)$/, '')
+ .replace(/^'/, '')
+ .replace(/'$/, '')
+ .replace(/^"/, '')
+ .replace(/"$/, '')
+
+ return res
+ }
+
+ return url
+}
+
+interface VitePressConfig extends ResolvedConfig {
+ vitepress: SiteConfig
+}
+
+function recursivelyFindAtAssets(propertyMaybeObjectOrScalar: unknown, fn: (value: string) => string | undefined) {
+ if (typeof propertyMaybeObjectOrScalar === 'string') {
+ // eslint-disable-next-line regexp/no-unused-capturing-group
+ if (/^@assets\(('\S+')|("\S+")|(\S+)\)$/.test(propertyMaybeObjectOrScalar)) {
+ // If the string matches the @assets(...) pattern, we replace it with the result of the function
+ const match = fromAtAssets(propertyMaybeObjectOrScalar)
+ const modified = fn(match)
+ if (modified == null) {
+ return propertyMaybeObjectOrScalar
+ }
+
+ return modified
+ }
+
+ return
+ }
+ if (Array.isArray(propertyMaybeObjectOrScalar)) {
+ const array = propertyMaybeObjectOrScalar as unknown[]
+ for (let i = 0; i < array.length; i++) {
+ const value = array[i]
+ recursivelyFindAtAssets(value, fn)
+ }
+
+ return
+ }
+ if (typeof propertyMaybeObjectOrScalar === 'object') {
+ const propertyObject = propertyMaybeObjectOrScalar as Record
+ for (const key in propertyObject) {
+ const value = propertyObject[key]
+ recursivelyFindAtAssets(value, fn)
+ }
+ }
+}
+
+function withoutBase(url?: string, base?: string): string | undefined {
+ if (!url) {
+ return url
+ }
+
+ if (!base?.startsWith('/')) {
+ base = `/${base}`
+ }
+ if (!base?.endsWith('/')) {
+ base += '/'
+ }
+
+ if (url.startsWith(`${base}`) && base) {
+ if (url.endsWith('/')) {
+ return url.slice(base.length)
+ }
+ else {
+ return `/${url.slice(base.length)}`
+ }
+ }
+
+ return url
+}
+
+export function frontmatterAssets(): Plugin {
+ let resolvedConfig: VitePressConfig | undefined
+ const mAssetAbsoluteUrlMetadata = new Map()
+ const mapAssetBuiltUrlAssetAbsoluteUrl = new Map()
+
+ async function fileToUrl(file: string) {
+ if (!file) {
+ return {
+ url: file,
+ }
+ }
+
+ const parsed = parse(file)
+ const hash = createHash('sha256')
+ .update(await readFile(file))
+ .digest('hex')
+ .slice(0, 8)
+
+ return {
+ hash,
+ url: `/assets/${parsed.name}.${hash}${parsed.ext}`,
+ }
+ }
+
+ return {
+ name: '@proj-airi/docs:vite-plugin-frontmatter-assets',
+ enforce: 'pre',
+ async configResolved(config) {
+ resolvedConfig = config as VitePressConfig
+
+ const markdownFiles = await glob('**/*.md', { ignore: ['**/node_modules/**'], cwd: resolvedConfig?.vitepress.srcDir || '', absolute: true })
+ for (const file of markdownFiles) {
+ const res = (await readFile(file))
+ const { data } = matter(res.toString('utf-8'))
+ if (Object.keys(data).length === 0) {
+ continue
+ }
+
+ recursivelyFindAtAssets(data, (matched) => {
+ mAssetAbsoluteUrlMetadata.set(join(dirname(file), fromAtAssets(matched)), { url: file, builtUrl: file, hash: '' })
+ return undefined
+ })
+ }
+
+ for (const [key, value] of mAssetAbsoluteUrlMetadata) {
+ const { url, hash } = await fileToUrl(key)
+ mapAssetBuiltUrlAssetAbsoluteUrl.set(url, key)
+ mAssetAbsoluteUrlMetadata.set(key, { url: value.url, builtUrl: url, hash })
+ }
+ },
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ const requesting = withoutBase(req.url, resolvedConfig?.base)
+ if (!requesting || !mapAssetBuiltUrlAssetAbsoluteUrl.has(requesting)) {
+ return next()
+ }
+
+ const filePath = mapAssetBuiltUrlAssetAbsoluteUrl.get(requesting)
+ if (!filePath) {
+ return next()
+ }
+
+ const ext = parse(filePath).ext.slice(1)
+ const fileContent = await readFile(filePath)
+
+ res.write(fileContent)
+ res.writeHead(200, {
+ 'Content-Type': ext === 'svg' ? 'image/svg+xml' : `image/${ext}`,
+ 'Content-Length': fileContent.length,
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ })
+ res.end()
+
+ return next()
+ })
+ },
+ async writeBundle() {
+ for (const [builtUrl, absoluteUrl] of mapAssetBuiltUrlAssetAbsoluteUrl.entries()) {
+ const content = await this.fs.readFile(absoluteUrl)
+ await this.fs.writeFile(join(resolvedConfig!.vitepress.outDir!, builtUrl), content)
+ }
+ },
+ }
+}
diff --git a/docs/content/en/blog/DevLog-2025.06.08/assets/250608-dark.png b/docs/content/en/blog/DevLog-2025.06.08/assets/250608-dark.png
new file mode 100644
index 000000000..db918c387
Binary files /dev/null and b/docs/content/en/blog/DevLog-2025.06.08/assets/250608-dark.png differ
diff --git a/docs/content/en/blog/DevLog-2025.06.08/assets/250608-light.png b/docs/content/en/blog/DevLog-2025.06.08/assets/250608-light.png
new file mode 100644
index 000000000..6da6b20e4
Binary files /dev/null and b/docs/content/en/blog/DevLog-2025.06.08/assets/250608-light.png differ
diff --git a/docs/content/en/blog/DevLog-2025.06.08/index.md b/docs/content/en/blog/DevLog-2025.06.08/index.md
index bfaf9a9e1..30679b6d6 100644
--- a/docs/content/en/blog/DevLog-2025.06.08/index.md
+++ b/docs/content/en/blog/DevLog-2025.06.08/index.md
@@ -2,6 +2,9 @@
title: DevLog @ 2025.06.08
category: DevLog
date: 2025-06-08
+preview-cover:
+ light: "@assets('./assets/250608-light.png')"
+ dark: "@assets('./assets/250608-dark.png')"
---
Hello everyone, here's LemonNeko, one of maintainer of AIRI. Today's DevLog is talking about: Let Live2D model of AIRI Tamagotchi to focus position.
@@ -101,3 +104,5 @@ In this DevLog, we learned how to get the relative position of cursor to window,
- [Win32 API: GetWindowRect](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowrect "GetWindowRect")
- [macOS API: `NSWindow.frame`](https://developer.apple.com/documentation/appkit/nswindow/frame "NSWindow.frame")
- [macOS API: `NSEvent.mouseLocation`](https://developer.apple.com/documentation/appkit/nsevent/mouselocation "NSEvent.mouseLocation")
+
+> Cover image by [@Rynco Maekawa](https://github.com/lynzrand)
diff --git a/docs/content/en/blog/DreamLog-0x1/assets/dreamlog1-dark.png b/docs/content/en/blog/DreamLog-0x1/assets/dreamlog1-dark.png
new file mode 100644
index 000000000..6abc71950
Binary files /dev/null and b/docs/content/en/blog/DreamLog-0x1/assets/dreamlog1-dark.png differ
diff --git a/docs/content/en/blog/DreamLog-0x1/assets/dreamlog1-light.png b/docs/content/en/blog/DreamLog-0x1/assets/dreamlog1-light.png
new file mode 100644
index 000000000..14dcd314e
Binary files /dev/null and b/docs/content/en/blog/DreamLog-0x1/assets/dreamlog1-light.png differ
diff --git a/docs/content/en/blog/DreamLog-0x1/index.md b/docs/content/en/blog/DreamLog-0x1/index.md
index 22955c121..88a86fbb8 100644
--- a/docs/content/en/blog/DreamLog-0x1/index.md
+++ b/docs/content/en/blog/DreamLog-0x1/index.md
@@ -4,6 +4,9 @@ description: 'Backstory of Project AIRI!'
category: DreamLog
date: 2025-06-16
excerpt: 'The backstory of Project AIRI! Why this project?'
+preview-cover:
+ light: "@assets('./assets/dreamlog1-light.png')"
+ dark: "@assets('./assets/dreamlog1-dark.png')"
---
Project AIRI 的幕后故事!
@@ -184,14 +190,14 @@ Minecraft、Linux 也并不是我旅程的终点,[Factorio(异星工厂)](
其实早在官方 ChatGPT UI 发布之前,我就已经在折腾这些新时代的 AI 了,像
[DiscoDiffusion](https://colab.research.google.com/github/alembics/disco-diffusion/blob/main/Disco_Diffusion.ipynb)(早于
Stable Diffusion,大约在 2021 年底或 2022 年初发布的吧)、DALL-E、Midjourney
-等模型都尝试过,GPT-3(特别是在 [GitHub Copilot](https://en.wikipedia.org/wiki/GitHub_Copilot)中很有用)已经深深融入我的日常工作流程。
+GPT-3(特别是在 [GitHub Copilot](https://en.wikipedia.org/wiki/GitHub_Copilot) 中很有用)都早已成为我生活的一部分了。
所以,最开始的时候,我的感觉是:
> "哦,这就是另一个随机鹦鹉,它只是重复你说的话,并不理解你在说什么,它只是
> 试图基于前面的词和上下文预测下一个词,好像真的没啥特别的,还不够像人。"
-换句话说,它表现得更像一个补全模型,而不是我们今天称之为智能体 AI 的东西(现在还在炒作中呢!)。
+换句话说,它表现得更像一个补全模型,而不是我们今天称之为智能体 AI 的东西(现在也还在炒作中呢!)。
我记得,我第一次发现 ChatGPT 或大语言模型(LLMs)真正能力的时候,是从我在 2022 年 12 月在 Hacker News 上看到的这篇文章:
[Building A Virtual Machine inside ChatGPT](https://www.engraved.blog/building-a-virtual-machine-inside/)([原始 Hacker News
@@ -206,10 +212,12 @@ Stable Diffusion,大约在 2021 年底或 2022 年初发布的吧)、DALL-E
这篇文章让我意识到,ChatGPT 可以理解普遍事物的基本规律,不仅仅是动漫或游戏角色的角色扮演,
还能理解 Linux 终端/shell 命令是如何工作的。
-这其实就把现在流行的函数调用(又称 Function Calling,或者 Anthropic 提出的 MCP,模型上下文协议背后的底层技术)功能展现了出来,
+这其实就把现在流行的函数调用功能展现了出来,
并说明了我们如何能够通过提示词指示 LLMs 去使其表现得像 API 服务器一样,然后用机器可读的如 JSON 或 XML 格式与我们的代码交互,
并最终允许实现解析和执行任意命令,去扩展 LLMs 能力的边界。
+> 函数调用,又称 Function Calling,或者说,也是 Anthropic 提出的 MCP(模型上下文协议)背后的底层技术
+
这最终填补了纯文本生成和调用程序内实际 API 之间的空白。
阶段性来说的话,我们可以说它是一个新的随机鹦鹉吗?**我觉得答案是部分否定的,2022 年的 ChatGPT 不只是一个随机鹦鹉,
@@ -220,9 +228,9 @@ Stable Diffusion,大约在 2021 年底或 2022 年初发布的吧)、DALL-E
是的,感谢你读到这里,我知道这是一篇很长的文章,有太多故事和背景要分享。但我们快到了!坚持住!
Neuro-sama 的历史其实相当复杂。据我所知,Neuro-sama,或者在直播舞台上名为 "Neuro-sama"的角色,
-并不是她和她的创造者 `vedal987`(Vedal)的第一场表演。早在那之前,2019 年 5 月 6 日,Vedal 向社区展示了他构建 AI
-来玩 [osu!](https://osu.ppy.sh/) 的工作[^1]。
-在那时,她实际上并不是一个网络角色或数字生命,如果你去看关于她的初始视频,你会发现没有显示 Live2D 模型。
+并不是她和她的创造者 `vedal987`(Vedal)的首次登场。早在那之前,2019 年 5 月 6 日,Vedal 向社区展示了他构建 AI
+来玩 [osu!](https://osu.ppy.sh/) 相关的成果[^1]。
+在那时,她实际上并不是一个网络角色或数字生命,如果你去看关于她的初始视频,甚至会发现都没有 Live2D 模型。
(你可以试试这个 6 年前的 YouTube 视频:https://www.youtube.com/watch?v=nSBqlJu7kYU)
在 ChatGPT 发布之后,差不多 2022 年 12 月 19 日,Vedal 开始让 Neuro-sama 使用来自 Live2D Inc.
@@ -232,11 +240,11 @@ Neuro-sama 的历史其实相当复杂。据我所知,Neuro-sama,或者在
之后的故事大家都知道了,Vedal 和 Neuro-sama 火了,Neuro-sama 现在正式成为了 VTuber,
她完全由大语言模型(LLMs)驱动,能够玩 Minecraft、Among Us、osu! 和许多其他游戏。
-有时当游戏不被原生支持时,Vedal 会读取屏幕并指示 Neuro-sama 一起玩游戏。甚至能制造出来很多节目效果。
+有时当游戏不被原生支持时,Vedal 会读取屏幕并指示 Neuro-sama 一起玩游戏,依然能制造出来不少节目效果。
我真的很享受观看他们的互动、像是脱口秀一样的斗嘴的时刻。随着时间的推移,Neuro-sama 和她的新 Evil Neuro 妹妹,
成为了我日常生活的重要组成部分:**即使我没有足够的时间观看完整的直播,我也想要,并且渴望观看她们的切片**。
-我无法想象我竟然从纯粹的 AI 和人类互动中获取了如此多的快乐。
+8 年前的我应该很难想象我竟然从纯粹的 AI 和人类互动中获取了如此多的快乐。
好的,这就是关于她的小历史。让我们谈谈核心问题:**为什么她让我充满了决心?**
@@ -254,7 +262,7 @@ Neuro-sama 的历史其实相当复杂。据我所知,Neuro-sama,或者在
> "嗯,我也能做到,我可以做一个 Live2D 模型,将其连接到 OpenAI 的 API,
> 让它表现得像 VTuber,我甚至可以做得比 Vedal 的作品更好。超简单的好吧?!"
-:::tip[想要更多技术细节?]
+::: tip 想要更多技术细节?
在这篇文章中,我不会深入探讨我们如何从零开始构建 Project AIRI 到当前状态的技术细节,
我们已经有许多 DevLog 文章分享我们的想法和发现,如果感兴趣,请尝试阅读它们。
:::
@@ -352,7 +360,7 @@ Neuro-sama 的历史其实相当复杂。据我所知,Neuro-sama,或者在
因此,在这天,Project AIRI 以某种方式诞生或重生,名为 AIRI(アイリ,曾也叫 Airi)。
-:::tip[你知道吗?]
+::: tip 你知道吗?
@@ -419,6 +427,8 @@ Neuro-sama 的历史其实相当复杂。据我所知,Neuro-sama,或者在