わたし、《ことは ひかる》!
CSSとJavaScriptがだ~いすきなエンジニア。
ある日、Qiitaを眺めながらCSSを書いてたら、突然、 謎のプラットフォーム を使った キャンペーンに出会ったのっ!
…これを使えば、「自然言語処理」 できるの!?キラやば~っ☆
Qiitaではフロントエンドエンジニアたちが、おしゃれな記事を書いて共有しているらしいのだけど…
あるとき、目的をはき違えた センスのかけらもない記事 が、Qiitaに投稿されてしまったの!
「CSSの表現力をもっと伝えたい」そう強く思った瞬間、 《構文解析》 を諦めて、 《要約(β)》、《感情分析》、《キーワード抽出》、 《固有表現抽出》 の結果を使って、
文字列が長いほうから、タグ付けきの文字列に置換して、キラキラなCSSを書いていったら、
わたし、「COTOHA DECO」作っちゃった!?
よーしっ、さっそくQiitaへ投稿だーっ☆
このスクリプトの実行結果が、次のCodePenのHTMLだよっ☆
const request = require('request');
const fs = require('fs');
// flat Polyfill
if (!Array.prototype.flat) {
Array.prototype.flat = function(depth) {
var flattend = [];
(function flat(array, depth) {
for (let el of array) {
if (Array.isArray(el) && depth > 0) {
flat(el, depth - 1);
} else {
flattend.push(el);
}
}
})(this, Math.floor(depth) || 1);
return flattend;
};
}
const DEVELOPER_API_BASE_URL = "https://api.ce-cotoha.com/api/dev/";
const ACCESS_TOKEN_PUBLISH_URL = "https://api.ce-cotoha.com/v1/oauth/accesstokens";
const CLIENT_ID = "ココニアイデイカク";
const CLIENT_SECRET = "ココニシークレットカク";
const main = async () => {
let accessToken = await getAccessToken();
await cotohaMultiParse(accessToken, 'star');
}
const cotohaMultiParse = async (accessToken, folderName) => {
console.log(`■${folderName} のフォルダに対する処理を実施します。`);
// let document = fs.readFileSync(`${folderName}/00_raw.txt`, 'utf-8');
let document = `
わたし、《星奈ひかる》!
宇宙と星座がだ~いすきな中学2年生。
星空を観察しながらノートに星座を描いていたら
とつぜん謎の生物《フワ》がワープしてきたのっ!
それから、空からロケットが落ちてきて、
宇宙人の《ララ》と《プルンス》までやってきた!
…あなたたち、ホンモノの宇宙人!?キラやば~っ☆
地球から遠くとおく離れた《星空界》の
中心部にある聖域《スターパレス》では
《12星座のスタープリンセスたち》が
全宇宙の均衡を保っていたらしいのだけど…
あるとき何者かに襲われて、プリンセスたちは
《12本のプリンセススターカラーペン》になって
宇宙に散らばってしまったの!
このままじゃ星が消えて、地球も宇宙も、闇に飲み込まれちゃう…!
『星々の輝きが失われし時、
トゥインクルブックと共に現る戦士プリキュアが再びの輝きを取り戻す』
ララ達は宇宙に古くから伝わる伝説を頼りに
プリンセスが最後に生み出した希望・フワと一緒に
《伝説の戦士・プリキュア》を探していたんだって。
そこへ宇宙の支配を目論むノットレイダーがフワを狙って襲いかかってきて…
「フワを助けたい!」そう強く思った瞬間、《トゥインクルブック》から
《スターカラーペンダント》と《変身スターカラーペン》が現れて、
わたし、プリキュアに変身しちゃった!?
宇宙に散らばったプリンセススターカラーペンを集めて、
スタープリンセス復活の鍵となるフワを育てなきゃ!
よーしっ、地球を飛び出して宇宙へ出発だーっ☆
`
let summary = await getSummary(accessToken, folderName,document.replace(/\n/g, "。"), 3); // 改行を文区切り扱いするため。
let summaries = summary.split('。').filter(s =>s.length > 0);
let ne = await getNe(accessToken, folderName, document);
let keyword = await getKeyword(accessToken, folderName, document);
let sentiment = await getSentiment(accessToken, folderName, document);
// apiの結果は主に全角なため、置換を繰り返してタグをつけても問題ない前提(ちゃんとやる場合も、事前処理や全角/半角を工夫して利用すれば処理できるか)
// キーワードは長いものから処理をしないと、置換したタグで分割されてしまうことに注意
const getStarTag = form => {
let num = Math.floor((form.length - 2) / 2) + 1;
let ret = []
for (let i = 0; i < num; i++) {
let animeRotate = `anime-rotate-${Math.floor(Math.random() * 3) + 1}-${Math.floor(Math.random() * 3) + 1}`;
let color = `color${Math.floor(Math.random() * 4) + 1}`;
ret.push(`<span class="star" style="left: ${((i * 2)+ (Math.random() * 1.4) - 0.8).toFixed(3)}em; top: ${((Math.random() * 1.2) - 0.6).toFixed(3)}em;"><span class="star-base bk"><span class="star-raw bk ${animeRotate} ${color}"></span></span><span class="star-base"><span class="star-raw ${animeRotate} ${color}"></span></span></span>`);
}
return ret.join('');
}
const getKiraTag = form => {
let num = form.length;
let ret = []
for (let i = 0; i < num; i++) {
let animeFlash = `anime-flash-${Math.floor(Math.random() * 4) + 1}-${Math.floor(Math.random() * 3) + 1}`;
ret.push(`<span class="kira" style="left: ${((i)+ (Math.random() * 1.6) - 0.8).toFixed(3)}em; top: ${((Math.random() * 1.8) - 0.9).toFixed(3)}em;"><span class="kira-raw-1 ${animeFlash}"></span><span class="kira-raw-2 ${animeFlash}"></span></span>`);
}
return ret.join('');
}
let kira = ``
let total = [
summaries.map( s => { return { form: s, after: `<span class="summary">${s}</span>` } }),
ne.map( n => { return { form: n.form, after: `<span class="ne ne-${n.class}">${n.form}</span>` } }),
keyword.map( k => { return { form: k.form, after: `<span class="keyword" data-score="${k.score}">${getStarTag(k.form)}${k.form}</span>` } }),
sentiment.emotional_phrase.filter(s => s.emotion === 'P' || s.emotion === 'PN' || s.emotion === '喜ぶ').map( e => { return { form: e.form, after: `<span class="emotion">${getKiraTag(e.form)}${e.form}</span>` } }),
].flat().sort( (a, b) => b.form.length - a.form.length );
total.forEach( s => {
document = document.replace(new RegExp(s.form, 'gi'), s.after)
});
console.log(document)
document = document.split('\n').map(d => d.length > 0 ? `<div><span class="sentence">${d}</span></div>` : '<br/>').join('');
// hmlt作成処理
fs.writeFileSync(`${folderName}/${folderName}.html`, `
<html>
<head>
<meta charset="UTF-8">
<title>${folderName}</title>
<style>
body {
background: repeating-linear-gradient(38deg, rgb(255, 174, 201, 0.4), rgb(255, 174, 201, 0.4) 24px, rgb(255, 64, 128, 0.4) 24px, rgb(255, 64, 128, 0.4) 48px);
font-family:"ヒラギノ丸ゴ Pro W4","ヒラギノ丸ゴ Pro","Hiragino Maru Gothic Pro","ヒラギノ角ゴ Pro W3","Hiragino Kaku Gothic Pro","HG丸ゴシックM-PRO","HGMaruGothicMPRO";
line-height: 2.2em
}
div {
text-align:center
}
br{
line-height: 1em;
}
span.sentence {
background: linear-gradient(transparent, rgb(255, 255, 255, 0.4) 16%, rgb(255, 255, 255, 0.5) 50%, rgb(255, 255, 255, 0.4) 16%, transparent);
padding: 8px 8px;
border-radius: 16px;
z-index: 100;
}
.summary {
font-size: 1.6em;
background: linear-gradient(transparent, rgb(255, 255, 255, 0.4) 20%, rgb(255, 255, 255, 0.5) 50%, rgb(255, 255, 255, 0.4) 80%, transparent);
margin: 0px -8px;
padding: 8px 8px;
border-radius: 4px;
}
.ne {
font-weight: bold;
color: rgb(255, 83, 169);
}
.keyword {
position: relative;
}
.emotion {
position: relative;
}
.ne {
position: relative;
}
.star {
position: absolute;
}
.star-base {
top: 2px;
left: 3px;
height: 16px;
width: 16px;
position: absolute;
overflow:hidden;
border-radius: 8px;
}
.star-base.bk {
top: 0px;
left: 0px;
height: 21px;
width: 21px;
border-radius: 10px;
}
.star-raw {
margin: 10px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
display: block;
height: 0;
width: 0;
position: absolute;
left: -12px;
top: -6px;
z-index: -1;
}
.star-raw.color1 {
border-bottom: 7px solid rgba(251, 176, 4, 1);
}
.star-raw.color2 {
border-bottom: 7px solid rgba(5, 186, 159, 1);
}
.star-raw.color3 {
border-bottom: 7px solid rgba(33, 138, 254, 1);
}
.star-raw.color4 {
border-bottom: 7px solid rgba(255, 45, 150, 1);
}
.star-raw:before,
.star-raw:after {
border-left: 10px solid transparent;
border-right: 10px solid transparent;
content: '';
display: block;
height: 0;
left: -10px;
position: absolute;
top: 0;
width: 0;
z-index: -1;
}
.star-raw.color1:before,
.star-raw.color1:after {
border-bottom: 7px solid rgba(251, 176, 4, 1);
}
.star-raw.color2:before,
.star-raw.color2:after {
border-bottom: 7px solid rgba(5, 186, 159, 1);
}
.star-raw.color3:before,
.star-raw.color3:after {
border-bottom: 7px solid rgba(33, 138, 254, 1);
}
.star-raw.color4:before,
.star-raw.color4:after {
border-bottom: 7px solid rgba(255, 45, 150, 1);
}
.star-raw:before {
transform: rotate(71deg);
}
.star-raw:after {
transform: rotate(-71deg);
}
.star-raw.bk {
margin: 12px;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-bottom: 10px solid rgba(255, 233, 182, 0.4);
left: -14px;
top: -8px;
}
.star-raw.bk:before,
.star-raw.bk:after {
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-bottom: 10px solid rgba(255, 233, 182, 0.4);
left: -12px;
}
.anime-rotate-1-1 {
animation: anime-rotate-1 7s linear infinite
}
.anime-rotate-1-2 {
animation: anime-rotate-1 10s linear infinite
}
.anime-rotate-1-3 {
animation: anime-rotate-1 30s linear infinite
}
.anime-rotate-2-1 {
animation: anime-rotate-2 7s linear infinite
}
.anime-rotate-2-2 {
animation: anime-rotate-2 10s linear infinite
}
.anime-rotate-2-3 {
animation: anime-rotate-2 30s linear infinite
}
.anime-rotate-3-1 {
animation: anime-rotate-3 7s linear infinite
}
.anime-rotate-3-2 {
animation: anime-rotate-3 10s linear infinite
}
.anime-rotate-3-3 {
animation: anime-rotate-3 30s linear infinite
}
@keyframes anime-rotate-1 {
0% { transform: rotate(35deg);}
50% { transform: rotate(125deg);}
100% { transform: rotate(35deg);}
}
@keyframes anime-rotate-2 {
0% { transform: rotate(45deg);}
50% { transform: rotate(105deg);}
100% { transform: rotate(45deg);}
}
@keyframes anime-rotate-3 {
0% { transform: rotate(55deg);}
50% { transform: rotate(175deg);}
100% { transform: rotate(55deg);}
}
.anime-flash-1-1 {
animation: anime-flash-1 3s linear infinite
}
.anime-flash-1-2 {
animation: anime-flash-1 5s linear infinite
}
.anime-flash-1-3 {
animation: anime-flash-1 10s linear infinite
}
.anime-flash-2-1 {
animation: anime-flash-2 3s linear infinite
}
.anime-flash-2-2 {
animation: anime-flash-2 5s linear infinite
}
.anime-flash-2-3 {
animation: anime-flash-2 10s linear infinite
}
.anime-flash-3-1 {
animation: anime-flash-3 3s linear infinite
}
.anime-flash-3-2 {
animation: anime-flash-3 5s linear infinite
}
.anime-flash-3-3 {
animation: anime-flash-4 10s linear infinite
}
.anime-flash-4-1 {
animation: anime-flash-4 3s linear infinite
}
.anime-flash-4-2 {
animation: anime-flash-4 50s linear infinite
}
.anime-flash-4-3 {
animation: anime-flash-4 10s linear infinite
}
@keyframes anime-flash-1 {
0% { opacity: 1;}
10% { opacity: 0.2;}
90% { opacity: 0.2;}
100% { opacity: 1;}
}
@keyframes anime-flash-2 {
0% { opacity: 0.2;}
20% { opacity: 1;}
40% { opacity: 0.2;}
100% { opacity: 0.2;}
}
@keyframes anime-flash-3 {
0% { opacity: 0.2;}
45% { opacity: 0.2;}
75% { opacity: 1;}
95% { opacity: 0.2;}
100% { opacity: 0.2;}
}
@keyframes anime-flash-4 {
0% { opacity: 0.2;}
40% { opacity: 0.2;}
50% { opacity: 1;}
60% { opacity: 0.2;}
100% { opacity: 0.2;}
}
.kira {
position: absolute;
}
.kira-raw-1 {
margin: 10px;
display: block;
height: 0;
width: 0;
position: absolute;
left: 0;
top: 0;
z-index: -1;
}
.kira-raw-1:before,
.kira-raw-1:after {
border-left: 2px solid transparent;
border-right: 2px solid transparent;
border-bottom: 10px solid rgba(255, 255, 0, 1);
content: '';
display: block;
height: 0;
left: -2px;
position: absolute;
width: 0;
z-index: -1;
}
.kira-raw-1:after {
top: 10px;
transform: rotate(180deg);
}
.kira-raw-2 {
margin: 10px;
display: block;
height: 0;
width: 0;
position: absolute;
left: 0;
top: 0;
z-index: -1;
}
.kira-raw-2:before,
.kira-raw-2:after {
border-right:8px solid rgba(255, 255, 0, 1);
border-top: 2px solid transparent;
border-bottom: 2px solid transparent;
content: '';
display: block;
height: 0;
left: -8px;
top: 8px;
position: absolute;
width: 0;
z-index: -1;
}
.kira-raw-2:after {
left: 0px;
transform: rotate(180deg);
}
</style>
</head>
<body>
<div class="background"></div>
${document}
</body>
</html>
`);
return document;
}
const getAccessToken = () => {
return new Promise((resolve, reject) => {
request(
{
url: ACCESS_TOKEN_PUBLISH_URL,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
json: {
grantType: "client_credentials",
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
},
},
(error, response, body) => {
if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
if (typeof body !== 'object') body = JSON.parse(body);
resolve(body.access_token);
} else {
if (error) {
console.log(`request fail. error: ${error}`);
} else {
console.log(`request fail. response.statusCode: ${response.statusCode}, ${body}`);
}
reject(body);
}
},
);
});
}
const getCommonRequest = (url ,fileName, getRequestJsonFunc) => (accessToken, folderName, document) => {
let promise;
promise = new Promise((resolve, reject) => {
let filePath = `${folderName}/${fileName}`;
if (fs.existsSync(filePath)) {
resolve(JSON.parse(fs.readFileSync(filePath, 'utf-8')));
} else {
request(
{
url: `${DEVELOPER_API_BASE_URL}${url}`,
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=UTF-8', Authorization: `Bearer ${accessToken}`},
json: getRequestJsonFunc(document),
},
(error, response, body) => {
if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
if (typeof body !== 'object') body = JSON.parse(body);
if (body.status === 0) {
fs.writeFileSync(`${filePath}`, JSON.stringify(body.result, null, ' '));
resolve(body.result);
} else {
console.log(`request coreference fail. error: ${body.message}`);
reject(body);
}
} else {
if (error) {
console.log(`request ${url} fail. error: ${error}`);
} else {
msg = (typeof body !== 'object') ? body : JSON.stringify(body);
console.log(`request ${url} fail. response.statusCode: ${response.statusCode}, ${msg}`);
}
reject(body);
}
}
);
}
});
return promise;
}
const getNe = (accessToken, folderName, document) => {
return getCommonRequest('nlp/v1/ne', '20_ne_raw.json', document => {
return { sentence: document };
})(accessToken, folderName, document);
}
const getKeyword = (accessToken, folderName, document) => {
return getCommonRequest('nlp/v1/keyword', '50_keyword_raw.json', document => {
return {
document: document,
max_keyword_num: 8,
};
})(accessToken, folderName, document);
}
const getSentiment = (accessToken, folderName, document) => {
return getCommonRequest('nlp/v1/sentiment', '60_sentiment_raw.json', document => {
return { sentence: document };
})(accessToken, folderName, document);
}
const get3Summary = async (accessToken, folderName, document) => {
let summary1 = await getSummary(accessToken, folderName, document, 1);
let summary2 = await getSummary(accessToken, folderName, document, 2);
let summary3 = await getSummary(accessToken, folderName, document, 3);
let summary2Part = summary2.replace(summary1, '');
let summary2Array = summary2Part.split('。').filter(v => v.length > 0).map(v => v + '。');
let summary3Part = summary3.replace(summary1, '');
summary2Array.forEach(s => {
summary3Part = summary3Part.replace(s, '');
});
let summary3Array = summary3Part.split('。').filter(v => v.length > 0).map(v => v + '。');
// summary1と、summary2とsummary3は。で区切られた、1文、2文、3文にはなっているが、
// summary2にsummary1が含まれないこともある。
return [ [ { form: summary1, sent_len: 1 } ] , summary2Array.map(s => { return { form: s, sent_len: 2}; } ), summary3Array.map(s => { return { form: s, sent_len: 3}; } )].flat();
}
const getSummary = (accessToken, folderName, document, i) => {
return getCommonRequest('nlp/beta/summary', `40_summary_${i}_raw.json`, document => {
return {
document: document,
sent_len: i,
};
})(accessToken, folderName, document);
}
main();
See the Pen NWqaGWe by j5c8k6m8 (@j5c8k6m8) on CodePen.
参考リンク
スター☆トゥインクルプリキュア 作品情報 - 東映アニメーション
CSSだけで色々な星を再現する(おまけ付き) - Qiita
おまけ
ラグビーW杯 決勝のニュース
https://www.nikkei.com/article/DGXMZO51758280S9A101C1000000/
See the Pen abOLvmB by j5c8k6m8 (@j5c8k6m8) on CodePen.
外郎売