Skip to content

Commit a70542e

Browse files
committed
first commit
0 parents  commit a70542e

18 files changed

+3495
-0
lines changed

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

.gitignore

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
```
14+
15+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16+
17+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
18+
19+
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
20+
21+
## Learn More
22+
23+
To learn more about Next.js, take a look at the following resources:
24+
25+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27+
28+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29+
30+
## Deploy on Vercel
31+
32+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33+
34+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

app/Home.tsx

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
'use client'
2+
import { FFmpeg } from '@ffmpeg/ffmpeg'
3+
import { fetchFile, toBlobURL } from '@ffmpeg/util'
4+
import { useEffect, useRef, useState } from 'react'
5+
import { toast } from 'react-toastify'
6+
import { FaGithub } from "react-icons/fa"
7+
import { LLM, getCommandArray } from './utils/llm'
8+
9+
export default function Home() {
10+
const logDivRef = useRef(null)
11+
const ffmpegRef = useRef(new FFmpeg())
12+
const [ffmpegLog, setffmpegLog] = useState<string[]>([])
13+
const [files, setFiles] = useState([])
14+
const [prompt, setPrompt] = useState('')
15+
const [llmConfig, setllmConfig] = useState({
16+
endpoint: '',
17+
key: '',
18+
model: ''
19+
})
20+
const llm = new LLM(llmConfig.endpoint, llmConfig.key, llmConfig.model)
21+
22+
const printLog = (message: string) => {
23+
setffmpegLog((prev) => [...prev, message])
24+
}
25+
26+
const handleFileChange = (event) => {
27+
setFiles(Array.from(event.target.files))
28+
}
29+
30+
useEffect(() => {
31+
if (logDivRef.current) {
32+
logDivRef.current.scrollTop = logDivRef.current.scrollHeight
33+
}
34+
}, [ffmpegLog])
35+
36+
useEffect(() => {
37+
if (localStorage.getItem('llmConfig')) {
38+
setllmConfig(JSON.parse(localStorage.getItem('llmConfig')))
39+
}
40+
else {
41+
setllmConfig({
42+
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
43+
key: '',
44+
model: 'llama3-8b-8192'
45+
})
46+
}
47+
48+
const baseURL = 'https://unpkg.com/@ffmpeg/[email protected]/dist/esm'
49+
const ffmpeg = ffmpegRef.current
50+
ffmpeg.on('log', ({ message }) => {
51+
if (message.startsWith("frame=")) {
52+
printLog(message + "_frame")
53+
return
54+
} else if (message.startsWith("[adts @") || message.startsWith("[mp4 @")) {
55+
printLog(message + "_time")
56+
return
57+
} else if (message.endsWith("error reading header")) {
58+
printLog("errorReadingHeader")
59+
return
60+
}
61+
printLog(message)
62+
})
63+
64+
const loadffmpeg = async () => {
65+
// toBlobURL is used to bypass CORS issue
66+
toast.info('ffmpeg加载中...' )
67+
printLog('ffmpeg加载中...')
68+
await ffmpeg.load({
69+
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
70+
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
71+
})
72+
toast.success('ffmpeg加载完成,版本 ffmpeg core v0.12.6', { toastId: 'ffmpeg' })
73+
printLog('ffmpeg加载完成,版本 ffmpeg core v0.12.6')
74+
}
75+
loadffmpeg()
76+
}, [])
77+
78+
const ffmpegRun = async () => {
79+
let cmdArray = []
80+
if (prompt.startsWith('!') || prompt.startsWith('!')) {
81+
cmdArray = getCommandArray(prompt.slice(1))
82+
}
83+
else {
84+
try {
85+
cmdArray = await llm.requestModel(files.map(file => file.name).join(','), prompt)
86+
console.log(files.map(file => file.name).join(','), prompt)
87+
printLog(cmdArray.join(' '))
88+
toast.info('开始处理: ' + cmdArray.join(' '))
89+
} catch (error) {
90+
console.error(error)
91+
toast.error('请求模型失败,请检查配置')
92+
return
93+
}
94+
}
95+
//删除cmdArray中的ffmpeg
96+
cmdArray.shift()
97+
console.log('cmdArray--------------', cmdArray)
98+
try {
99+
const ffmpeg = ffmpegRef.current
100+
for (let file of files) {
101+
await ffmpeg.writeFile(file.name, await fetchFile(file))
102+
}
103+
let dirList = await ffmpeg.listDir("./")
104+
console.log('dirList', dirList)
105+
await ffmpeg.exec(cmdArray)
106+
let output = await ffmpeg.listDir("./")
107+
output = output.filter(file => dirList.map(dir => dir.name).indexOf(file.name) === -1)
108+
console.log('output', output)
109+
printLog('处理完成')
110+
toast.success('处理完成')
111+
112+
for (let file of output) {
113+
toast.info('文件' + file.name + '已生成')
114+
console.log('文件' + file.name + '已生成')
115+
let data = await ffmpeg.readFile(file.name)
116+
const url = URL.createObjectURL(new Blob([data]))
117+
const a = document.createElement('a')
118+
a.href = url
119+
a.download = file.name
120+
a.click()
121+
}
122+
} catch (error) {
123+
console.error(error)
124+
toast.error('处理失败')
125+
}
126+
}
127+
128+
return (
129+
<>
130+
<div className="max-w-3xl mx-auto my-12 space-y-6">
131+
<div className="text-center space-y-2">
132+
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">ffmpeg.zh_CN</h1>
133+
<div className=' flex flex-row justify-center'>
134+
<p className="text-gray-500 dark:text-gray-400">使用自然语言与 ffmpeg wasm 在浏览器中编辑视频。</p>
135+
<a href="https://github.com/Souls-R/ffmpeg.zh_CN" target="_blank" rel="noopener noreferrer">
136+
<FaGithub className="mx-2 my-1 text-base hover:cursor-pointer" />
137+
</a>
138+
</div>
139+
</div>
140+
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
141+
<div className="p-4 space-y-6">
142+
<div className="collapse collapse-arrow">
143+
<input type="checkbox" />
144+
<div className="collapse-title text-xl font-medium">
145+
# LLM配置
146+
</div>
147+
<div className="collapse-content">
148+
<input
149+
className="input input-bordered w-full my-1"
150+
type="text"
151+
value={llmConfig.endpoint}
152+
placeholder='api端点地址'
153+
onChange={(event) => {
154+
setllmConfig({ ...llmConfig, endpoint: event.target.value })
155+
localStorage.setItem('llmConfig', JSON.stringify({ ...llmConfig, endpoint: event.target.value }))
156+
}}
157+
/>
158+
<input
159+
className="input input-bordered w-full my-1"
160+
type="text"
161+
placeholder='api密钥'
162+
value={llmConfig.key}
163+
onChange={(event) => {
164+
setllmConfig({ ...llmConfig, key: event.target.value })
165+
localStorage.setItem('llmConfig', JSON.stringify({ ...llmConfig, key: event.target.value }))
166+
}}
167+
/>
168+
<input
169+
className="input input-bordered w-full my-1"
170+
type="text"
171+
placeholder='模型'
172+
value={llmConfig.model}
173+
onChange={(event) => {
174+
setllmConfig({ ...llmConfig, model: event.target.value })
175+
localStorage.setItem('llmConfig', JSON.stringify({ ...llmConfig, model: event.target.value }))
176+
}}
177+
/>
178+
</div>
179+
</div>
180+
181+
<div className="px-3.5 space-y-4">
182+
<div
183+
className=" text-xl font-medium"
184+
>
185+
# 文件区域
186+
</div>
187+
<input
188+
className="file-input w-full file-input-bordered"
189+
multiple={true}
190+
type="file"
191+
onChange={handleFileChange}
192+
/>
193+
{files.map((file, index) => (
194+
<div key={index} className="badge badge-outline mx-0.5">
195+
{file.name}
196+
</div>
197+
))}
198+
<div>
199+
<div
200+
className=" text-xl font-medium mb-2"
201+
>
202+
# 想要怎样处理呢?
203+
</div>
204+
<textarea
205+
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[100px]"
206+
id="ffmpeg-settings"
207+
placeholder={"将输入的视频的第三秒到第五秒转换为gif格式,分辨率为720p,码率为1Mbps,减少帧率为10fps。\n\n或者以!开头,直接使用ffmpeg命令。"}
208+
onChange={(event) => setPrompt(event.target.value)}
209+
value={prompt}
210+
>
211+
</textarea>
212+
</div>
213+
<button
214+
className="btn btn-outline w-full"
215+
onClick={ffmpegRun}
216+
>
217+
开始处理
218+
</button>
219+
<div
220+
ref={logDivRef}
221+
className="flex flex-col w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background min-h-[100px] max-h-[30vh] overflow-y-auto whitespace-pre-line"
222+
>
223+
{ffmpegLog.map((log, index) => (
224+
<div key={index} style={{ whiteSpace: 'pre-line' }}>{log}</div>
225+
))}
226+
</div>
227+
</div>
228+
</div>
229+
</div>
230+
</div>
231+
</>
232+
)
233+
}
234+

app/NoSSRWrapper.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import dynamic from 'next/dynamic'
2+
import React from 'react'
3+
const NoSSRWrapper = props => (
4+
<React.Fragment>{props.children}</React.Fragment>
5+
)
6+
export default dynamic(() => Promise.resolve(NoSSRWrapper), {
7+
ssr: false
8+
})

app/favicon.ico

15 KB
Binary file not shown.

app/globals.css

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
/* 隐藏滚动条 */
6+
::-webkit-scrollbar {
7+
display: none;
8+
}
9+
10+
/* 隐藏滚动条,但仍然允许滚动 */
11+
body {
12+
-ms-overflow-style: none; /* IE and Edge */
13+
scrollbar-width: none; /* Firefox */
14+
}

app/layout.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import './globals.css'
2+
import type { Metadata } from 'next'
3+
import { Inter } from 'next/font/google'
4+
5+
const inter = Inter({ subsets: ['latin'] })
6+
7+
export const metadata: Metadata = {
8+
title: 'ffmpeg.zh_CN',
9+
description: '使用自然语言与 ffmpeg wasm 在浏览器中编辑视频。',
10+
}
11+
12+
export default function RootLayout({
13+
children,
14+
}: {
15+
children: React.ReactNode
16+
}) {
17+
return (
18+
<html>
19+
<body className={inter.className}>{children}</body>
20+
</html>
21+
)
22+
}

app/page.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use client'
2+
3+
import NoSSRWrapper from "./NoSSRWrapper";
4+
import Home from "./Home";
5+
import { ToastContainer } from 'react-toastify';
6+
import 'react-toastify/dist/ReactToastify.css';
7+
8+
export default function Page() {
9+
return <NoSSRWrapper><Home /><ToastContainer autoClose={10000} /></NoSSRWrapper>
10+
}

0 commit comments

Comments
 (0)