This commit is contained in:
lunaticbum 2025-03-19 18:27:39 +09:00
parent 931ec332d7
commit 2e3667d0a1
13 changed files with 3676 additions and 3345 deletions

View File

@ -55,6 +55,7 @@ dependencies {
implementation ("org.seleniumhq.selenium:selenium-java:4.10.0") implementation ("org.seleniumhq.selenium:selenium-java:4.10.0")
implementation ("org.commonmark:commonmark:0.18.0")
implementation ("com.drewnoakes:metadata-extractor:2.19.0") implementation ("com.drewnoakes:metadata-extractor:2.19.0")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")

View File

@ -87,8 +87,8 @@ class BumsInterceptor : HandlerInterceptor {
request.cookies?.forEach { request.cookies?.forEach {
if (it.name.equals("CLEAR", true)) { if (it.name.equals("CLEAR", true)) {
request.getSession(true)?.let { session -> request.getSession(false)?.let { session ->
session.invalidate() // session.invalidate()
session.setAttribute(WRITE_PERMISSION_KEY, false) session.setAttribute(WRITE_PERMISSION_KEY, false)
} }
} }

View File

@ -46,13 +46,16 @@ class SecurityConfig {
logService.log(it.toString()) logService.log(it.toString())
it.requestMatchers(HttpMethod.POST,"/user/**").permitAll() it.requestMatchers(HttpMethod.POST,"/user/**").permitAll()
// it.requestMatchers(HttpMethod.POST,"/user/**").permitAll() // it.requestMatchers(HttpMethod.POST,"/user/**").permitAll()
// it.requestMatchers(HttpMethod.POST,"/user/**").permitAll()
// it.requestMatchers("/", "/user/**").permitAll() // it.requestMatchers("/", "/user/**").permitAll()
// .requestMatchers(".ajax").permitAll() // .requestMatchers(".ajax").permitAll()
// it.requestMatchers("/", "/user/joinUser.api").permitAll() // it.requestMatchers("/", "/user/joinUser.api").permitAll()
// it.requestMatchers("user/joinUser.api").permitAll() // it.requestMatchers("user/joinUser.api").permitAll()
it.requestMatchers("/blog/viewer/**").permitAll()
it.anyRequest().permitAll() it.anyRequest().permitAll()
// .requestMatchers("/", "/login/**").permitAll() // .requestMatchers("/", "/login/**").permitAll()
// .requestMatchers("/posts/**", "/api/v1/posts/**").hasRole(Role.USER.name)
// .requestMatchers("/admins/**", "/api/v1/admins/**").hasRole(Role.ADMIN.name) // .requestMatchers("/admins/**", "/api/v1/admins/**").hasRole(Role.ADMIN.name)
// .anyRequest().authenticated() // .anyRequest().authenticated()
} }

View File

@ -5,6 +5,11 @@ import jakarta.servlet.http.HttpServletResponse
import kr.lunaticbum.back.lun.model.PostManageg import kr.lunaticbum.back.lun.model.PostManageg
import kr.lunaticbum.back.lun.model.ResultMV import kr.lunaticbum.back.lun.model.ResultMV
import kr.lunaticbum.back.lun.utils.LogService import kr.lunaticbum.back.lun.utils.LogService
import org.commonmark.node.Node
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
@ -25,13 +30,24 @@ class Home {
@GetMapping("/","/home") @GetMapping("/","/home")
fun home() : ResultMV { fun home() : ResultMV {
val vm = ResultMV("content/home") val vm = ResultMV("content/home")
vm.modelMap.put("Posts", postManageg.find20(Pageable.ofSize(20)).apply { try {
this.forEach { vm.modelMap.put("Posts", postManageg.find4().apply {
it.title = URLDecoder.decode(it.title) this.forEach {
it.content = URLDecoder.decode(it.content) it.title = URLDecoder.decode(it.title)
logService.log(Gson().toJson(it)) it.content = URLDecoder.decode(it.content)
} val parser: Parser = Parser.builder().build()
}) val document: Node = parser.parse(it.content)
val renderer = HtmlRenderer.builder().build()
Jsoup.parse(renderer.render(document))?.let { doc ->
val firstImg: Element? = doc.select("img")?.first()
val imgSrc: String = firstImg?.attr("src") ?: ""
it.image = imgSrc
it.html = doc.text()
}
it.title = if ((it.title?.length ?: 0) >= 1) it.title else ""
}
}.chunked(2))
}catch (ex: Exception){ex.printStackTrace()}
vm.modelMap.put("path","/blog/viewer/") vm.modelMap.put("path","/blog/viewer/")
return vm return vm
} }

View File

@ -312,8 +312,8 @@ class Telegram {
.bodyToMono(String::class.java).block() .bodyToMono(String::class.java).block()
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
var originalQuery = msg.text var originalQuery = msg.text ?: ""
lama.generateResponse(originalQuery?.replace("오늘","오늘(${SimpleDateFormat("yyyy-MM-dd").format(Date())})"),msg.from?.id.toString()) lama.generateResponse(originalQuery.replace("오늘","오늘(${SimpleDateFormat("yyyy-MM-dd").format(Date())})"),msg.from?.id.toString())
} }
} }
@ -371,7 +371,7 @@ class Telegram {
// } // }
// } // }
CoroutineScope(Dispatchers.IO).async { CoroutineScope(Dispatchers.IO).async {
lama.generateResponse(originalQuery?.replace("오늘","오늘(${SimpleDateFormat("yyyy-MM-dd").format(Date())})")) lama.generateResponse(originalQuery.replace("오늘","오늘(${SimpleDateFormat("yyyy-MM-dd").format(Date())})"))
} }
return "TEST" return "TEST"
} }

View File

@ -34,6 +34,9 @@ class Post {
var category : String? = null var category : String? = null
var tags : String? = null var tags : String? = null
var html : String? = null
var image : String? = null
var writer : String? = null var writer : String? = null
var writeTime : Long = 0 var writeTime : Long = 0
var posting : Boolean = false var posting : Boolean = false
@ -69,6 +72,19 @@ class PostManageg {
return postRepository.findAllByPostingTrue(pageable).takeLast(20).buffer(20).blockLast(Duration.ofSeconds(30)) ?: listOf() return postRepository.findAllByPostingTrue(pageable).takeLast(20).buffer(20).blockLast(Duration.ofSeconds(30)) ?: listOf()
} }
fun find4() : List<Post> {
val originalList = postRepository.findAllByModifyTime(0)
.takeLast(4)
.buffer(4)
.blockLast(Duration.ofSeconds(30)) ?: listOf()
return originalList + List((4 - originalList.size).coerceAtLeast(0)) {
Post() // 기본값 생성 (필드 초기화 필요)
}
// return postRepository.findAllByModifyTime(0).takeLast(4).buffer(4).blockLast(Duration.ofSeconds(30)) ?: listOf()
}
fun find20() : List<Post> { fun find20() : List<Post> {
return postRepository.findAllByModifyTime(0).takeLast(20).buffer(20).blockLast(Duration.ofSeconds(30)) ?: listOf() return postRepository.findAllByModifyTime(0).takeLast(20).buffer(20).blockLast(Duration.ofSeconds(30)) ?: listOf()
} }

View File

@ -2,22 +2,21 @@ package kr.lunaticbum.back.lun.service
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import io.micrometer.observation.ObservationRegistry import io.micrometer.observation.ObservationRegistry
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.launch
import kr.lunaticbum.back.lun.configs.GlobalEnvironment import kr.lunaticbum.back.lun.configs.GlobalEnvironment
import kr.lunaticbum.back.lun.controllers.TelegramSendMsg import kr.lunaticbum.back.lun.controllers.TelegramSendMsg
import kr.lunaticbum.back.lun.model.* import kr.lunaticbum.back.lun.model.*
import kr.lunaticbum.back.lun.utils.RssFeedsParser import kr.lunaticbum.back.lun.utils.RssFeedsParser
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements import org.jsoup.select.Elements
import org.openqa.selenium.By import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.remote.RemoteWebDriver import org.openqa.selenium.remote.RemoteWebDriver
import org.springframework.ai.embedding.EmbeddingRequest import org.springframework.ai.embedding.EmbeddingRequest
@ -37,6 +36,8 @@ import java.net.URLEncoder
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.regex.Matcher
import java.util.regex.Pattern
@Service @Service
@ -84,54 +85,108 @@ class Lama {
val joinString = "\n#" val joinString = "\n#"
var lastElements : Elements = Elements() var lastElements : Elements = Elements()
var body = Jsoup.connect(url).timeout(30000).get().body() var body = Jsoup.connect(url).timeout(30000).get().body()
// var elements : Elements? = null var elements : Elements? = null
// if (url.contains("nate.com", true)) { if (url.contains("nate.com", true)) {
// if (url.contains("view", true)) { if (url.contains("view", true)) {
// elements = body.select("[class*=articleView]") elements = body.select("[class*=articleView]")
// }else { }else {
// elements = body.select("[class*=postRankSubjectList]") elements = body.select("[class*=postRankSubjectList]")
// } }
// } else if (url.contains("newsis.com/view", true)) { } else if (url.contains("newsis.com/view", true)) {
// elements = body.select("[class*=articleView]") elements = body.select("[class*=articleView]")
// } else if (url.contains("blog.naver.com", true)) { } else if (url.contains("blog.naver.com", true)) {
// elements = body.select("[class*=se-viewer]") elements = body.select("[class*=se-viewer]")
// } else if (url.contains("bbc.com/korean/articles", true)) { } else if (url.contains("bbc.com/korean/articles", true)) {
// elements = body.select("main[role$=main]") elements = body.select("main[role$=main]")
// } else if (url.contains("chosun.com/client", true)) { } else if (url.contains("chosun.com/client", true)) {
// elements = body.select("[class*=articleBody]") elements = body.select("[class*=articleBody]")
// } else if (url.contains("nocutnews.co.kr/news", true)) { } else if (url.contains("nocutnews.co.kr/news", true)) {
// elements = body.select("[class*=container]") elements = body.select("[class*=container]")
// } else if (url.contains("hani.co.kr/arti/", true)) { } else if (url.contains("hani.co.kr/arti/", true)) {
// elements = body.select("[class*=ArticleDetail]") elements = body.select("[class*=ArticleDetail]")
// } else if (url.contains("yna.co.kr/view", true)) { } else if (url.contains("yna.co.kr/view", true)) {
// elements = body.select("[class*=container]") elements = body.select("[class*=container]")
// } else if (url.contains("newspim.com/news", true)) { } else if (url.contains("newspim.com/news", true)) {
// elements = body.select("[class*=container]") elements = body.select("[class*=container]")
// } else {
//
// }
// if (elements?.size ?: 0 > 0) {
// elements?.forEach {
// lastElements.add(it)
// }
// }
//
// if (lastElements.size < 1) {
// arrayOf("container","article","main","viewer","content").forEach {
// var result = Elements()
// result.addAll(body.select("[class*=$it]"))
// result.addAll(body.select("[id*=$it]"))
// result.addAll(body.select(it))
// result.forEach { if (it.text().length > 100 && it.children().size < 5) { lastElements.add(it) } }
// }
// }
return if (lastElements.size > 0) {
lastElements.eachText().joinToString(joinString)
} else { } else {
body.children().eachText().joinToString(joinString)
}
if (elements?.size ?: 0 > 0) {
elements?.forEach {
lastElements.add(it)
}
}
if (lastElements.size < 1) {
arrayOf("container","article","main","viewer","content").forEach {
var result = Elements()
result.addAll(body.select("[class*=$it]"))
result.addAll(body.select("[id*=$it]"))
result.addAll(body.select(it))
result.forEach { if (it.text().length > 100 && it.children().size < 5) { lastElements.add(it) } }
}
}
return if (lastElements.size > 0) {
lastElements.text()
} else {
body.text()
} }
} }
fun jsopFilter(doc : Document) : String {
var url = doc.baseUri()
val joinString = "\n#"
var lastElements : Elements = Elements()
var body = doc
var elements : Elements? = null
if (url.contains("nate.com", true)) {
if (url.contains("view", true)) {
elements = body.select("[class*=articleView]")
}else {
elements = body.select("[class*=postRankSubjectList]")
}
} else if (url.contains("newsis.com/view", true)) {
elements = body.select("[class*=articleView]")
} else if (url.contains("blog.naver.com", true)) {
elements = body.select("[class*=se-viewer]")
} else if (url.contains("bbc.com/korean/articles", true)) {
elements = body.select("main[role$=main]")
} else if (url.contains("chosun.com/client", true)) {
elements = body.select("[class*=articleBody]")
} else if (url.contains("nocutnews.co.kr/news", true)) {
elements = body.select("[class*=container]")
} else if (url.contains("hani.co.kr/arti/", true)) {
elements = body.select("[class*=ArticleDetail]")
} else if (url.contains("yna.co.kr/view", true)) {
elements = body.select("[class*=container]")
} else if (url.contains("newspim.com/news", true)) {
elements = body.select("[class*=container]")
} else {
}
if (elements?.size ?: 0 > 0) {
elements?.forEach {
lastElements.add(it)
}
}
if (lastElements.size < 1) {
arrayOf("container","article","main","viewer","content").forEach {
var result = Elements()
result.addAll(body.select("[class*=$it]"))
result.addAll(body.select("[id*=$it]"))
result.addAll(body.select(it))
result.forEach { if (it.text().length > 100 && it.children().size < 5) { lastElements.add(it) } }
}
}
return if (lastElements.size > 0) {
lastElements.text()
} else {
body.text()
}
}
// class WebScrap { // class WebScrap {
// @SerializedName("query", alternate = ["question"]) // @SerializedName("query", alternate = ["question"])
// var query: String? = null // var query: String? = null
@ -153,17 +208,65 @@ class Lama {
val llmPhi4Mini = "phi4-mini" val llmPhi4Mini = "phi4-mini"
val llmDolphin3 = "dolphin3" val llmDolphin3 = "dolphin3"
var llm_gemma3_4b = "gemma3:4b"
var llm_phi4_mini = "phi4-mini:latest"
var llm_dolphin3 = "dolphin3:latest"
var llm_gemma3_12b = "gemma3:12b"
var llm_phi4_14b = "phi4:14b"
var llm_mistral_7b = "mistral:7b"
val currentLLM = llmDolphin3
val currentLLM = llm_dolphin3
fun getGoogleSearch(query:String){ fun getGoogleSearch(query:String){
Jsoup.connect("https://www.google.com/search?q=".plus(query)).timeout(30000).get().select("a[href]").forEach { } Jsoup.connect("https://www.google.com/search?q=".plus(query)).timeout(30000).get().select("a[href]").forEach { }
} }
val waitTime = 1000L val waitTime = 1500L
val topCount = 2 val topCount = 2
fun webDriver() : RemoteWebDriver {
val options : ChromeOptions = ChromeOptions();
options.addArguments("--headless");
options.addArguments("--disable-popup-blocking");
options.addArguments("--disable-default-apps");
options.addArguments("--disable-notifications");
options.addArguments("--disable-blink-features=AutomationControlled")
return RemoteWebDriver(URL("https://video.lunaticbum.kr"), options)
}
fun isValidUrl(url: String): Boolean {
val urlRegex = "^(https?|ftp)://[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$".toRegex()
return url.matches(urlRegex)
}
@Async
suspend fun getterUrl(urlString: String) {
try {
webDriver()?.let { driver ->
var findCount = 0
try {
driver.get("urlString");
Thread.sleep(waitTime)
println(driver.currentUrl)
driver.findElement(By.ByTagName("Body"))?.let { webElement ->
Jsoup.parse(driver.pageSource).select("[href*=https]").forEach {
var href = it.attr("href")
println(href)
}
}
}catch (e:Exception){
e.printStackTrace()
}
driver.close()
driver.quit()
}
}catch (e:Exception){}
}
@Async @Async
suspend fun addDocuments(query : String , refinedQuery: RefinedQuery?) { suspend fun addDocuments(query : String , refinedQuery: RefinedQuery?) {
var querys : ArrayList<String> = ArrayList() var querys : ArrayList<String> = ArrayList()
@ -171,48 +274,65 @@ class Lama {
refinedQuery?.ko_query?.let { querys.add(it) } refinedQuery?.ko_query?.let { querys.add(it) }
refinedQuery?.en_query?.let { querys.add(it) } refinedQuery?.en_query?.let { querys.add(it) }
refinedQuery?.keywords?.let { querys.add(it.joinToString { " " })} refinedQuery?.ko_keywords?.let { querys.add(it.joinToString( " "))}
refinedQuery?.en_keywords?.let { querys.add(it.joinToString( " "))}
val readedUrls = ArrayList<String>() val readedUrls = ArrayList<String>()
try { try {
var options : ChromeOptions = ChromeOptions();
options.addArguments("--disable-popup-blocking");
options.addArguments("--disable-default-apps");
options.addArguments("--disable-notifications");
options.addArguments("--disable-blink-features=AutomationControlled");
val targetUrls = hashSetOf<String>() val targetUrls = hashSetOf<String>()
RemoteWebDriver(URL("https://video.lunaticbum.kr"), options).let { driver ->
querys.forEach { refinedQuery-> querys.forEach { refinedQuery->
var findCount = 0 try {
try { webDriver()?.let { driver ->
driver.get("https://www.google.com/search?q=$refinedQuery"); var findCount = 0
Thread.sleep(waitTime) try {
println(driver.currentUrl) driver.get("https://www.google.com/search?q=$refinedQuery");
driver.findElement(By.ByTagName("Body"))?.let { webElement -> Thread.sleep(waitTime)
Jsoup.parse(driver.pageSource).select("[href*=https]").forEach { println(driver.currentUrl)
var href = it.attr("href") driver.findElement(By.ByTagName("Body"))?.let { webElement ->
if (href?.length ?: 0 > 5 && href.startsWith("https://") && findCount < topCount && href.contains("google") == false && href.contains("youtube") == false) { Jsoup.parse(driver.pageSource).select("[href*=https]").forEach {
targetUrls.add(href) var href = it.attr("href")
println("add targetUrls $href") if (href?.length ?: 0 > 5 && href.startsWith("https://") && findCount < topCount && href.contains("google") == false && href.contains("youtube") == false) {
findCount += 1 targetUrls.add(href)
println("add targetUrls $href")
findCount += 1
}
} }
} }
}
}catch (e:Exception){ }catch (e:Exception){
e.printStackTrace() e.printStackTrace()
}
driver.close()
driver.quit()
} }
} }catch (e:Exception){}
driver.close()
driver.quit()
} }
options = ChromeOptions();
options.addArguments("--disable-popup-blocking"); querys.forEach { refinedQuery ->
options.addArguments("--disable-default-apps"); try {
options.addArguments("--disable-notifications"); webDriver()?.let { driver ->
options.addArguments("--disable-blink-features=AutomationControlled"); var findCount = 0
RemoteWebDriver(URL("https://video.lunaticbum.kr"), options).let { driver -> RssFeedsParser().readFeed("https://news.google.com/rss/search?q=${URLEncoder.encode(refinedQuery)}=ko&gl=KR&ceid=KR%3Ako/")?.messages?.forEach {
targetUrls.forEach { url -> var url: String? = it.link
if (url?.length ?: 0 > 5 && url?.startsWith("https://") == true && readedUrls.contains(url) == false && findCount < topCount) {
println("url >>>> $url")
targetUrls.add(url!!)
findCount += 1
}
}
driver.close()
driver.quit()
}
}catch (e:Exception){
}
}
targetUrls.forEach { url ->
webDriver()?.let { driver ->
var result = SearXngResult() var result = SearXngResult()
if (url?.length ?: 0 > 5 && url?.startsWith("https://") == true && readedUrls.contains(url) == false) { if (url?.length ?: 0 > 5 && url?.startsWith("https://") == true && readedUrls.contains(url) == false) {
readedUrls.add(url!!) readedUrls.add(url!!)
@ -222,85 +342,47 @@ class Lama {
driver.get(url); driver.get(url);
Thread.sleep(waitTime) Thread.sleep(waitTime)
driver.findElement(By.ByTagName("Body"))?.let { webElement -> driver.findElement(By.ByTagName("Body"))?.let { webElement ->
if(webElement.text.length > 120) { jsopFilter(Jsoup.parse(driver.pageSource)).let { text ->
println(driver.currentUrl) result.originHtml = text
println(webElement.text) webPageSummarize(result)
result.title = driver.title
result.originHtml = webElement.text
webPageSummarize(result, webElement.text)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() // e.printStackTrace()
} }
} }
driver.close();
driver.quit()
} }
driver.close();
driver.quit()
} }
options = ChromeOptions();
options.addArguments("--disable-popup-blocking");
options.addArguments("--disable-default-apps");
options.addArguments("--disable-notifications");
options.addArguments("--disable-blink-features=AutomationControlled");
RemoteWebDriver(URL("https://video.lunaticbum.kr"), options).let { driver ->
querys.forEach { refinedQuery ->
var googleSCount = 0
RssFeedsParser().readFeed("https://news.google.com/rss/search?q=${URLEncoder.encode(query)}=ko&gl=KR&ceid=KR%3Ako/")?.messages?.forEach {
var url: String? = it.link
var result = SearXngResult()
println("url >>>> $url")
if (url?.length ?: 0 > 5 && url?.startsWith("https://") == true && readedUrls.contains(url) == false && googleSCount < topCount) {
readedUrls.add(url!!)
result.url = url!!
result.originQuery = query
result.refinedQuery = refinedQuery
result.title = it.title
println(result.title)
try {
driver.get(url);
Thread.sleep(waitTime)
println(driver.currentUrl)
driver.findElement(By.ByTagName("Body"))?.let { webElement ->
println(driver.currentUrl)
println(webElement.text)
result.title = driver.title
result.originHtml = webElement.text
webPageSummarize(result, webElement.text)
googleSCount += 1
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
driver.close()
driver.quit()
}
} catch (e:Exception){e.printStackTrace()} } catch (e:Exception){e.printStackTrace()}
querys.forEach { refinedQuery -> querys.forEach { refinedQuery ->
val gSearch = "https://psn.lunaticbum.kr/search?q=${refinedQuery?.replace("오늘", SimpleDateFormat("yyyMMdd").format(Date()))}&language=ko&time_range=month&safesearch=0&categories=general&format=json" val gSearch = "https://psn.lunaticbum.kr/search?q=${refinedQuery?.replace("오늘", SimpleDateFormat("yyyMMdd").format(Date()))}&language=ko&time_range=month&safesearch=0&categories=general&format=json"
println("gSearch >>> ${gSearch}") // println("gSearch >>> ${gSearch}")
WebClient.create().get() WebClient.create().get()
.uri(gSearch) .uri(gSearch)
.retrieve() .retrieve()
.bodyToMono(SearXng::class.java).timeout(Duration.ofMinutes(20L)).block()?.let { gsResult -> .bodyToMono(SearXng::class.java).timeout(Duration.ofMinutes(20L)).block()?.let { gsResult ->
gsResult.results?.filter { it.url?.startsWith("https://") == true && it.score > 0.4 }?.forEach { gsResult.results?.filter { it.url?.startsWith("https://") == true && it.score > 5.0 }?.forEach {
println("in filter ${it.url}")
if (readedUrls.contains(it.url) == false) { if (readedUrls.contains(it.url) == false) {
readedUrls.add(it.url!!) readedUrls.add(it.url!!)
it.originQuery = query it.originQuery = query
it.refinedQuery = refinedQuery it.refinedQuery = refinedQuery
println(it.title)
try { try {
jsopFilter(it.url!!).let { text -> webDriver()?.let { driver ->
it.originHtml = text driver.get(it.url!!)
webPageSummarize(it, text) Thread.sleep(waitTime)
driver.findElement(By.ByTagName("Body"))?.let { webElement ->
jsopFilter(it.url!!).let { text ->
it.originHtml = text
webPageSummarize(it)
}
}
driver.close()
driver.quit()
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -308,37 +390,107 @@ class Lama {
} }
} }
} }
println("end of search") // println("end of search")
} }
} }
var format = "context:'%s'\ncontext는 웹 페이지 문자를 가져온 것 '%s'이 질문에 대해 연관 결과로 받은 내용임. 해당 context 정리 해서 본문 내용을 최대한 자세히 알려줘\n'{query:질문 내용, contents_ko:자세한 내용 한국어 , summary_ko:요약된 내용 한국어, keywords:[키워드], related_links:[{link,description}}], relatedness_score:0.0~10.0}'\n이 형식의 결과로 만들어 줘" var format = "\"context:'%s'\n" +
"The context is extracted text from a web page. '%s' is the content received as a relevant result for this question. Please analyze and summarize the given context in detail, and provide the following information in JSON format.\n" +
"\n" +
"Please provide the result in this format, ensuring that all information is in Korean language."
var webSummaryResultFormat : String = """{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Original question"
},
"contents_ko": {
"type": "string",
"description": "Detailed content in Korean"
},
"summary_ko": {
"type": "string",
"description": "Concise summary in Korean"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
"related_links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"link": { "type": "string" },
"description": { "type": "string" }
},
"required": ["link", "description"],
"additionalProperties": false
}
},
"relatedness_score": {
"type": "number",
"minimum": 0.0,
"maximum": 10.0
}
},
"required": ["query", "contents_ko", "summary_ko", "keywords", "related_links", "relatedness_score"],
"additionalProperties": false
}""";
internal fun makeSummarizeRequestMsg(it : SearXngResult) : String= format.format(it.originHtml,it.originQuery) internal fun makeSummarizeRequestMsg(it : SearXngResult) : String= format.format(it.originHtml,it.originQuery)
internal fun makeCahtReq(reqMsg:String) = OllamaApi.ChatRequest.Builder(currentLLM).stream(false).format("json").messages(reqMsg.chunked(100).map { println(it); OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content(it).build()}.toList()).build() internal fun makeCahtReq(reqMsg:String, ollamaOptions: OllamaOptions?, format: Any) = OllamaApi.ChatRequest.Builder(currentLLM).options(ollamaOptions).stream(false).format(format).messages(reqMsg.chunked(100).map { OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content(it).build()}.toList()).build()
var options = OllamaOptions.builder().build()//.temperature(0.8).topK(3).seed(30)
@Async @Async
fun webPageSummarize(it : SearXngResult , text : String) { fun webPageSummarize(it : SearXngResult) {
try { try {
infomationDic.get(it.originQuery)!!.put(it.url!!, text) // println("send to blama >> ${it.url}")
val chatClient = OllamaApi("https://lama.lunaticbum.kr") infomationDic.get(it.originQuery)?.put(it.url!!,Gson().toJson(it))
val embeddingModel = OllamaEmbeddingModel(chatClient, OllamaOptions.builder().build(), ObservationRegistry.create(), ModelManagementOptions.defaults()) try {
val embeddingResponse = embeddingModel.call(EmbeddingRequest(text.chunked(400).toList(), OllamaOptions.builder().model(currentEmbedimg).truncate(false).build())) CoroutineScope(Dispatchers.IO).launch {
it.originHtml = text val chatClient = OllamaApi("https://lama.lunaticbum.kr")
val sdss = QPut(arrayListOf()) chatClient.chat(makeCahtReq(makeSummarizeRequestMsg(it), options, ObjectMapper().readValue(webSummaryResultFormat,Map::class.java))).toMono().subscribe({aiResponce ->
sdss.points.add(QData(id = System.currentTimeMillis(), embeddingResponse.result.output, it)) it.pageData = aiResponce.message.content
if (sdss.points.size > 0) { var needSave = true
val qUrl = "https://ollama.lunaticbum.kr/collections/blama_vectors".plus("/points") try {
val client = WebClient.create() var jsonObj = JsonParser.parseString(aiResponce.message.content)
client.put() needSave = jsonObj.isJsonObject && (jsonObj as JsonObject)?.get("relatedness_score")?.asDouble ?: 0.0 > 0.5 } catch (e: Exception) {
.uri(qUrl) e.printStackTrace()
.header("api-key", "blama-admin-key-gb") }
.body(BodyInserters.fromValue(Gson().toJson(sdss))) if (needSave) {
.retrieve() sendTlg("유효한 정보가 수집됨.".plus(aiResponce.message.content))
.bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L)).subscribe( val embeddingModel = OllamaEmbeddingModel(chatClient, OllamaOptions.builder().build(), ObservationRegistry.create(), ModelManagementOptions.defaults())
{ resultString -> }, { error -> error.printStackTrace() } val embeddingResponse = embeddingModel.call(EmbeddingRequest(Gson().toJson(it).chunked(400).toList(), OllamaOptions.builder().model(currentEmbedimg).truncate(false).build()))
)
val sdss = QPut(arrayListOf())
sdss.points.add(QData(id = System.currentTimeMillis(), embeddingResponse.result.output, it))
if (sdss.points.size > 0) {
val qUrl = "https://ollama.lunaticbum.kr/collections/blama_vectors".plus("/points")
val client = WebClient.create()
client.put()
.uri(qUrl)
.header("api-key", "blama-admin-key-gb")
.body(BodyInserters.fromValue(Gson().toJson(sdss)))
.retrieve()
.bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L)).subscribe(
{ resultString -> }, { error -> error.printStackTrace() }
)
}}
},{err->
err.printStackTrace()
})
}
} }
catch (e:Exception){e.printStackTrace()}
}catch (e : Exception) { }catch (e : Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -347,16 +499,46 @@ class Lama {
class RefinedQuery { class RefinedQuery {
var ko_query : String? = null var ko_query : String? = null
var en_query : String? = null var en_query : String? = null
var keywords : Array<String>? = null var ko_keywords : Array<String>? = null
var en_keywords : Array<String>? = null
} }
var queryFormat = "질문:\n'%s'\n앞은 질문의 내용을 정리해서 '{ko_query:한국어 질문,en_query:영어 번역된 질문,ko_keywords:[한국어 키워드],en_keyword:[영문키워드]}'이 형식의 결과를 부탁할께"
var jsonSchema: String = """{
"type": "object",
"properties": {
"ko_query": {
"type": "string",
"description": "korean query"
},
"en_query": {
"type": "string",
"description": "english query"
},
"ko_keywords": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "korean keywords"
},
"en_keywords": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "query keyword"
}
},
"required": ["ko_query", "en_query", "ko_keywords", "en_keywords"],
"additionalProperties": false
}""".trimIndent()
var queryFormat = "Question:\n'%s'\nBased on the above question, please provide a JSON result formatted\nPlease ensure:\n1. Faithful translation maintaining original intent\n2. Keyword extraction focusing on core concepts\n3. Bilingual keyword matching\n4. Proper JSON formatting"
internal fun makeQuerySummarizeRequestMsg(query : String) : String= queryFormat.format(query) internal fun makeQuerySummarizeRequestMsg(query : String) : String= queryFormat.format(query)
fun querySummarize(query: String) : RefinedQuery? { fun querySummarize(query: String) : RefinedQuery? {
var refinedQuery : RefinedQuery? = null var refinedQuery : RefinedQuery? = null
try { try {
val chatClient = OllamaApi("https://lama.lunaticbum.kr") val chatClient = OllamaApi("https://lama.lunaticbum.kr")
var dispoable = chatClient.chat(makeCahtReq(makeQuerySummarizeRequestMsg(query))).toMono().subscribe({aiResponce -> var dispoable = chatClient.chat(makeCahtReq(makeQuerySummarizeRequestMsg(query),options, ObjectMapper().readValue(jsonSchema,Map::class.java))).toMono().subscribe({aiResponce ->
println("summary result >>>>> ${aiResponce.message.content}") println("summary result >>>>> ${aiResponce.message.content}")
refinedQuery = Gson().fromJson(aiResponce.message.content, RefinedQuery::class.java) refinedQuery = Gson().fromJson(aiResponce.message.content, RefinedQuery::class.java)
},{err-> },{err->
@ -380,7 +562,7 @@ class Lama {
.body(BodyInserters.fromValue(Gson().toJson(QSearchData(embedFlots,3)))) .body(BodyInserters.fromValue(Gson().toJson(QSearchData(embedFlots,3))))
.retrieve() .retrieve()
.bodyToMono(QSearch::class.java).timeout(Duration.ofMinutes(20L)).block() .bodyToMono(QSearch::class.java).timeout(Duration.ofMinutes(20L)).block()
println(Gson().toJson(lists))
return if (lists?.result?.size ?: 0 > 0) { return if (lists?.result?.size ?: 0 > 0) {
val qContents = QContentsList() val qContents = QContentsList()
lists?.result?.filter { it.score > 8.0 }?.forEach { qContents.ids.add(it.id) } lists?.result?.filter { it.score > 8.0 }?.forEach { qContents.ids.add(it.id) }
@ -400,64 +582,195 @@ class Lama {
@Autowired @Autowired
lateinit var globalEvv : GlobalEnvironment lateinit var globalEvv : GlobalEnvironment
val resultJsonScheme = """{
"type": "object",
"properties": {
"querys": {
"type": "array",
"items": {
"type": "string"
},
"description": "사용자의 질문 목록"
},
"answers": {
"type": "array",
"items": {
"type": "string"
},
"description": "질문에 대한 상세한 답변 목록"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "답변과 관련된 주요 키워드 목록"
},
"links": {
"type": "array",
"items": {
"type": "string"
},
"description": "참고할 만한 관련 링크 목록"
}
},
"required": ["querys", "answers", "keywords", "links"],
"additionalProperties": false
}""".trimIndent()
var infomationDic = hashMapOf<String,HashMap<String,String>>() var infomationDic = hashMapOf<String,HashMap<String,String>>()
suspend fun generateResponse(query: String?, targetId: String? = globalEvv.telegramMyId) { suspend fun generateResponse(query: String, targetId: String? = globalEvv.telegramMyId) {
val chatClient = OllamaApi("https://lama.lunaticbum.kr") if (isValidUrl(query)) {
val embeddingModel = OllamaEmbeddingModel( getterUrl(query)
chatClient, OllamaOptions.builder().build(), ObservationRegistry.create(), ModelManagementOptions.defaults()) }else {
println("On generateResponse :: find something ${query}")
query?.let { originalQuery ->
infomationDic.put(query!!, hashMapOf())
var embeddingResponse = embeddingModel.call(EmbeddingRequest(listOf(originalQuery), OllamaOptions.builder().model(currentEmbedimg).truncate(false).build()))
addDocuments(originalQuery, querySummarize(originalQuery))
println("points size ${embeddingResponse.result.output.size}")
var context : StringBuffer = StringBuffer()
try {
embedQuery(embeddingResponse.result.output)?.result?.forEach { result ->
if (infomationDic.get(query!!)!!.contains(result.payload?.url ?: "NONE") == false) {
context.append("\n# :".plus(if (result.payload?.pageData?.length ?: 0 > 10) {
result.payload?.pageData
} else {
result.payload?.content
}))
}
}
}catch (e:Exception){
e.printStackTrace()
}
infomationDic.get(query!!)!!.iterator().forEach { context.append("\n#${it.key}:${it.value}") }
val prompt : StringBuffer = StringBuffer().append("참조:\n").append(context).append("\n참조 내용을 고려 해서\n'$query'").append(query).append("\n에 {querys:[],answers:[],keywords:[],links:[]}형식으로 최대한 자세히 대답 해줘 모든 내용은 한국어로 해줘")
val fullUrl = "https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage" val fullUrl = "https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage"
val chatClient = OllamaApi("https://lama.lunaticbum.kr")
val embeddingModel = OllamaEmbeddingModel(
chatClient,
OllamaOptions.builder().build(),
ObservationRegistry.create(),
ModelManagementOptions.defaults()
)
println("On generateResponse :: find something ${query}")
val response: OllamaApi.ChatResponse = chatClient.chat(OllamaApi.ChatRequest.Builder(currentLLM).stream(false).format("json").messages( query.let { originalQuery ->
prompt.chunked(300).map { println(it); OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content(it).build()}.toList()).build()) infomationDic.put(query!!, hashMapOf())
// println(response.message.content)
CoroutineScope(Dispatchers.IO).launch { try {
var toalmsg = "${query}의 대답이 도착했어요.\n${response.message.content}" var embeddingResponse = embeddingModel.call(
toalmsg.chunked(512).forEach { chunkedMsg -> EmbeddingRequest(
println("chunkedMsg >>> ${chunkedMsg}") listOf(originalQuery),
(targetId ?: globalEvv.telegramMyId)?.let { OllamaOptions.builder().model(currentEmbedimg).truncate(false).build()
var tlgSend = TelegramSendMsg(it, chunkedMsg) )
WebClient )
.create(fullUrl) addDocuments(originalQuery, querySummarize(originalQuery))
.post() println("points size ${embeddingResponse.result.output.size}")
.contentType(MediaType.APPLICATION_JSON) var context: StringBuffer = StringBuffer()
.body(BodyInserters.fromValue(Gson().toJson(tlgSend))) embedQuery(embeddingResponse.result.output)?.result?.forEach { result ->
.retrieve().bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L)) if (infomationDic.get(query!!)!!.contains(result.payload?.url ?: "NONE") == false) {
.block()?.let { result -> context.append(
println("result >>> ${result}") "\nReference:#".plus(
} if (result.payload?.pageData?.length ?: 0 > 10) {
result.payload?.pageData
} else {
result.payload?.content
}
)
)
}
} }
infomationDic.get(query!!)!!.iterator()
.forEach { context.append("\nReference:#${it.key}:${it.value}") }
val prompt: StringBuffer = StringBuffer()
prompt.append(context)
prompt.append("\nConsidering the above reference, please answer the following question:\n'$query'\n\nProvide a detailed response in the following JSON format\nPlease ensure all content is in Korean language and as detailed as possible.")
val answers = StringBuffer()
chatClient.streamingChat(OllamaApi.ChatRequest.Builder(currentLLM).stream(true)
.format(ObjectMapper().readValue(resultJsonScheme, Map::class.java)).messages(
prompt.chunked(1024)
.map { OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content(it).build() }.toList()
).build()
).timeout(Duration.ofMinutes(20)).subscribe({ responce ->
answers.append(responce.message.content)
println("responce.message.content >>> ${answers.length}")
try {
if (answers.length % 100 == 0) {
var tlgSend = TelegramSendMsg(
targetId ?: globalEvv.telegramMyId!!,
"결과를 수집 중 ${responce.message.content.length}의 문자열이 추가됨. 수집된 통 데이터는 ${answers.length}"
)
WebClient
.create(fullUrl)
.post()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(Gson().toJson(tlgSend)))
.retrieve().bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L))
.block()?.let { result ->
println("result >>> ${result}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}, { error ->
}, {
var toalmsg: StringBuffer = StringBuffer("${query}의 대답이 도착했어요.\n").append(answers.toString())
sendTlg(toalmsg.toString())
infomationDic.remove(query)
println("On generateResponse :: END OF Answer")
})
} catch (e: Exception) {
e.printStackTrace()
} }
infomationDic.remove(query!!)
} }
println("On generateResponse :: END OF Answer")
} }
} }
fun sendTlg(toalmsg : String) {
telegramScope.launch {
val fullUrl = "https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage"
toalmsg.chunked(512).forEach { chunkedMsg ->
launch {
println("chunkedMsg >>> ${chunkedMsg}")
globalEvv.telegramMyId?.let {
try {
var tlgSend = TelegramSendMsg(it, chunkedMsg)
WebClient
.create(fullUrl)
.post()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(Gson().toJson(tlgSend)))
.retrieve().bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L))
.awaitSingle()?.let { result ->
println("result >>> ${result}")
}
}catch (e:Exception){
e.printStackTrace()
}
}
}
}
}
}
private val telegramScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// fun sendTlg2(toalmsg: String) {
// telegramScope.launch {
// val fullUrl = "https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage"
// val chunkedMessages = toalmsg.chunked(512)
// chunkedMessages.forEach { chunkedMsg ->
// launch {
// try {
// globalEvv.telegramMyId?.let { chatId ->
// val tlgSend = TelegramSendMsg(chatId, chunkedMsg)
// val result = WebClient.create(fullUrl)
// .post()
// .contentType(MediaType.APPLICATION_JSON)
// .bodyValue(Gson().toJson(tlgSend))
// .retrieve()
// .awaitBody<String>()
//
// println("Telegram send success: $result")
// }
// } catch (e: Exception) {
// println("Telegram send failed: ${e.localizedMessage}")
// // 추가 에러 처리 로직
// }
// }
// }
// }
// }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,8 @@ function goToEditor(path,id,sk) {
location.href = path + id+"?token="+sk; location.href = path + id+"?token="+sk;
} }
function onclickWrite(type, keyword, html) { function onclickWrite(type, keyword, html) {
let title_field = document.getElementById('title_field') let title_field = document.getElementById('title_field')
var hasValues = true var hasValues = true

View File

@ -34,16 +34,25 @@
</script> </script>
</th:block> </th:block>
<th:block layout:fragment="content" id="content"> <th:block layout:fragment="content" id="content">
<div id="main_layer"> <section class="wrapper style1">
<label id="title_layer" class="write_option" ></label> <div class="container">
<div id="editor" ></div> <div id="content">
<div class="write_controllbox">
<div class="write_option btn-example" to="#popLayer1" onclick="openPopup(this)" ></div> <!-- Content -->
<div style="width: 15px" ></div>
<div class="write_option btn-example" to="#popLayer2" onclick="openPopup(this)" id="hashtag_field"></div> <article>
<div style="width: 15px" ></div> <label id="title_layer" class="write_option" ></label>
<label class="write_option" readonly id="location_field"></label> <div id="editor" ></div>
<div class="write_controllbox">
<div class="write_option btn-example" to="#popLayer1" onclick="openPopup(this)" ></div>
<div style="width: 15px" ></div>
<div class="write_option btn-example" to="#popLayer2" onclick="openPopup(this)" id="hashtag_field"></div>
<div style="width: 15px" ></div>
<label class="write_option" readonly id="location_field"></label>
</div>
</article>
</div>
</div> </div>
</div> </section>
</th:block> </th:block>
</html> </html>

View File

@ -10,25 +10,10 @@
<script type="text/javascript" th:src="@{/js/test.js}"></script> <script type="text/javascript" th:src="@{/js/test.js}"></script>
<link th:href="@{/css/toast-ui-dark.css}" rel="stylesheet" /> <link th:href="@{/css/toast-ui-dark.css}" rel="stylesheet" />
<script th:inline="javascript"> <script th:inline="javascript">
let editor
document.addEventListener("DOMContentLoaded", onLoaded); document.addEventListener("DOMContentLoaded", onLoaded);
function onLoaded() { function goToViewer(item) {
var els = document.getElementsByClassName('content') let uploadUrl = getMainPath() + "/blog/viewer/"+item.attributes['data'].value;
for (i=0;i<els.length;i++) { location.href = uploadUrl
var item = $(els[i])
console.log(item[0])
console.log(item.attr("data"))
new toastui.Editor({
el: item[0],
width : (item[0].getBoundingClientRect().width * 0.8) + 'px',
height : (item[0].getBoundingClientRect().width * 0.8) + 'px',
viewer: true,
usageStatistics : false,
initialValue: item.attr("data"),
theme:"dark",
});
}
} }
</script> </script>
</th:block> </th:block>
@ -36,7 +21,7 @@
<section id="banner"> <section id="banner">
<header> <header>
<h2>Bum's: <em>짧은 헛소리 혹은 기사?! 링크 있으면 링크까지</a></em></h2> <h2>Bum's: <em>짧은 헛소리 혹은 기사?! 링크 있으면 링크까지</a></em></h2>
<a href="#" class="button">더 읽으쉴?!<br>[Read More Gibberish]</a> <a href="#" class="button">더 읽으쉴?!<br>[Read More Gibberish]</a>
</header> </header>
</section> </section>
@ -52,7 +37,7 @@
</header> </header>
</div> </div>
</section> </section>
<section id="cta__3" class="wrapper style3"> <section id="cta2" class="wrapper style3">
<div class="container"> <div class="container">
<header> <header>
<h2>Are you ready to continue your quest?</h2> <h2>Are you ready to continue your quest?</h2>
@ -62,42 +47,14 @@
<!-- Posts --> <!-- Posts -->
<section class="wrapper style2"> <section class="wrapper style2">
<div class="container"> <div class="container">
<div class="row"> <div class="row" th:each="row : ${Posts}">
<section class="col-6 col-12-narrower"> <section class="col-6 col-12-narrower" th:each="post : ${row}">
<div class="box post"> <!-- <span th:text="${cell}"></span>-->
<a href="#" class="image left"><img src="images/pic01.jpg" alt="" /></a> <div class="box post" onclick="goToViewer(this)" th:data="${post.id}">
<a href="#" class="image left"><img th:src="${#strings.length(post.image) > 0} ? ${post.image} : 'images/pic01.jpg'" alt="" /></a>
<div class="inner"> <div class="inner">
<h3>The First Thing</h3> <h3 th:text="${#strings.length(post.title) > 0} ? ${post.title} : ('untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')} + ']')"></h3>
<p>Duis neque nisi, dapibus sed mattis et quis, nibh. Sed et dapibus nisl amet mattis, sed a rutrum accumsan sed. Suspendisse eu.</p> <p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
</div>
</div>
</section>
<section class="col-6 col-12-narrower">
<div class="box post">
<a href="#" class="image left"><img src="images/pic02.jpg" alt="" /></a>
<div class="inner">
<h3>The Second Thing</h3>
<p>Duis neque nisi, dapibus sed mattis et quis, nibh. Sed et dapibus nisl amet mattis, sed a rutrum accumsan sed. Suspendisse eu.</p>
</div>
</div>
</section>
</div>
<div class="row">
<section class="col-6 col-12-narrower">
<div class="box post">
<a href="#" class="image left"><img src="images/pic03.jpg" alt="" /></a>
<div class="inner">
<h3>The Third Thing</h3>
<p>Duis neque nisi, dapibus sed mattis et quis, nibh. Sed et dapibus nisl amet mattis, sed a rutrum accumsan sed. Suspendisse eu.</p>
</div>
</div>
</section>
<section class="col-6 col-12-narrower">
<div class="box post">
<a href="#" class="image left"><img src="images/pic04.jpg" alt="" /></a>
<div class="inner">
<h3>The Fourth Thing</h3>
<p>Duis neque nisi, dapibus sed mattis et quis, nibh. Sed et dapibus nisl amet mattis, sed a rutrum accumsan sed. Suspendisse eu.</p>
</div> </div>
</div> </div>
</section> </section>
@ -133,11 +90,10 @@
</div> </div>
</section> </section>
<!-- CTA --> <!-- CTA -->
<section id="cta" class="wrapper style3"> <section id="cta2" class="wrapper style3">
<div class="container"> <div class="container">
<header> <header>
<h2>Are you ready to continue your quest?</h2> <h2>Are you ready to continue your quest?</h2>
<a href="#" class="button">Insert Coin</a>
</header> </header>
</div> </div>
</section> </section>

View File

@ -64,7 +64,7 @@
<!-- Copyright --> <!-- Copyright -->
<div class="copyright"> <div class="copyright">
<ul class="menu"> <ul class="menu">
<li>&copy; Untitled. All rights reserved</li><li>Design: <a href="http://html5up.net">HTML5 UP</a></li> <li>&copy;lunaticbum. All rights reserved</li><li>Design: <a href="http://html5up.net">HTML5 UP</a></li>
</ul> </ul>
</div> </div>
</div> </div>