[mod] alles mögliche verbessert
This commit is contained in:
parent
b571c54195
commit
3497430fa4
13 changed files with 255 additions and 36 deletions
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
## Zu erledigen
|
## Zu erledigen
|
||||||
|
|
||||||
- asynchrones Erstellen der Audio-Dateien (solange nicht fertig, eine Markierung in der Liste anzeigen)
|
- sanitizing überprüfen
|
||||||
- Persistenz mit SQLite
|
- Persistenz mit SQLite
|
||||||
- Datum für Dokumente ergänzen
|
- Datum für Dokumente ergänzen
|
||||||
- eingebetter Media-Spieler?
|
|
||||||
- für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf/odt (pandoc nutzen) und ogg
|
- für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf/odt (pandoc nutzen) und ogg
|
||||||
- bei Löschung eines Dokuments auch die zugehörigen Dateien löschen
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ proof-of-concept für Partei-Arbeits-Dokumenten-Verwaltung, welche hörbare Vers
|
||||||
|
|
||||||
### Voraussetzungen
|
### Voraussetzungen
|
||||||
|
|
||||||
- curl (Debian-Paket-Name: `curl`)
|
- [curl](https://curl.se/docs/manpage.html) (Debian-Paket-Name: `curl`)
|
||||||
|
|
||||||
|
|
||||||
### Anweisungen
|
### Anweisungen
|
||||||
|
@ -33,9 +33,9 @@ proof-of-concept für Partei-Arbeits-Dokumenten-Verwaltung, welche hörbare Vers
|
||||||
|
|
||||||
### Voraussetzungen
|
### Voraussetzungen
|
||||||
|
|
||||||
- PHP auf Kommandozeile (Debian-Paket-Name: `php-cli`)
|
- [PHP](https://www.php.net/)-Interpreter für Kommandozeile (Debian-Paket-Name: `php-cli`)
|
||||||
- ffmpeg (Debian-Paket-Name: `ffmpeg`)
|
- [FFmpeg](https://ffmpeg.org/) (Debian-Paket-Name: `ffmpeg`)
|
||||||
- beliebigen Browser
|
- Browser
|
||||||
|
|
||||||
|
|
||||||
### Anweisungen
|
### Anweisungen
|
||||||
|
|
|
@ -6,6 +6,14 @@ require_once('string.php');
|
||||||
require_once('cache.php');
|
require_once('cache.php');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
function path_wrapped(string $path) : ?string
|
||||||
|
{
|
||||||
|
return (\file_exists($path) ? $path : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
function render(string $template_name, array $arguments) : string
|
function render(string $template_name, array $arguments) : string
|
||||||
|
@ -43,24 +51,36 @@ function navigate(string $target) : void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
function generate_audio(string $input, string $output_path) : void
|
function generate_audio(
|
||||||
|
string $input_value,
|
||||||
|
string $output_path,
|
||||||
|
?array $options = null
|
||||||
|
) : void
|
||||||
{
|
{
|
||||||
$key = \hash('sha256', $input);
|
$options = \array_merge(
|
||||||
$path_wav = \rosavox\helpers\string_\coin(
|
[
|
||||||
'/tmp/{{key}}.wav',
|
'blocking' => false,
|
||||||
|
],
|
||||||
|
($options ?? [])
|
||||||
|
);
|
||||||
|
$key = \hash('sha256', $input_value);
|
||||||
|
$input_path = \rosavox\helpers\string_\coin(
|
||||||
|
'/tmp/{{key}}.txt',
|
||||||
[
|
[
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
$path_ogg = $output_path;
|
\file_put_contents($input_path, $input_value);
|
||||||
$command = \rosavox\helpers\string_\coin(
|
$command = \rosavox\helpers\string_\coin(
|
||||||
'echo "{{input}}" | piper/piper --model piper/voice.onnx --output_file {{path_wav}} && ffmpeg -y -i {{path_wav}} {{path_ogg}} ; rm -f {{path_wav}}',
|
'scripts/generate-audio {{input_path}} {{output_path}}{{suffix}}',
|
||||||
[
|
[
|
||||||
'input' => $input,
|
'input_path' => $input_path,
|
||||||
'path_wav' => $path_wav,
|
'output_path' => $output_path,
|
||||||
'path_ogg' => $path_ogg,
|
'suffix' => ($options['blocking'] ? '' : ' > /dev/null 2>&1 &'),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
// \unlink($input_path);
|
||||||
|
// \error_log(\sprintf("-- %s\n", $command));
|
||||||
\exec($command);
|
\exec($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,19 @@ function coin(string $template, array $arguments) : string
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
function replace_all(
|
||||||
|
string $string,
|
||||||
|
array $replacements
|
||||||
|
) : string
|
||||||
|
{
|
||||||
|
$result = $string;
|
||||||
|
foreach ($replacement as $replacement_key => $replacement_value) {
|
||||||
|
$result = \str_replace($replacement_key, $replacement_value, $result);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -61,17 +61,105 @@ function render_list() : string
|
||||||
'docs-list-entry',
|
'docs-list-entry',
|
||||||
[
|
[
|
||||||
'label_read' => \rosavox\helpers\misc\translate('action.read'),
|
'label_read' => \rosavox\helpers\misc\translate('action.read'),
|
||||||
'label_hear' => \rosavox\helpers\misc\translate('action.hear'),
|
'value_text' => $entry['value']['title'],
|
||||||
'value_link_open' => make_link('edit', ['id' => \sprintf('%u', $entry['id'])]),
|
'value_link_open' => make_link('edit', ['id' => \sprintf('%u', $entry['id'])]),
|
||||||
'value_link_read' => \rosavox\services\doc\readable_path($entry['id']),
|
'value_link_read' => \rosavox\services\doc\readable_path($entry['id']),
|
||||||
'value_link_hear' => \rosavox\services\doc\audio_path($entry['id']),
|
'area_download_readable' => (
|
||||||
'value_name' => \rosavox\helpers\string_\coin(
|
(fn ($name, $path) => (
|
||||||
'{{name}}.oga',
|
(! \file_exists($path))
|
||||||
|
?
|
||||||
|
\rosavox\helpers\misc\render(
|
||||||
|
'state-waiting',
|
||||||
[
|
[
|
||||||
'name' => \rosavox\services\doc\name($entry['id']),
|
'info' => \rosavox\helpers\misc\translate('state.generating'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
:
|
||||||
|
\rosavox\helpers\misc\render(
|
||||||
|
'download',
|
||||||
|
[
|
||||||
|
'text' => \rosavox\helpers\misc\translate('item.readable.short'),
|
||||||
|
'tooltip' => \rosavox\helpers\string_\coin(
|
||||||
|
'{{item}} {{action}}',
|
||||||
|
[
|
||||||
|
'action' => \rosavox\helpers\misc\translate('action.download'),
|
||||||
|
'item' => \rosavox\helpers\misc\translate('item.readable.long'),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
'value_text' => $entry['value']['title'],
|
'link' => $path,
|
||||||
|
'name' => \rosavox\helpers\string_\coin(
|
||||||
|
'{{name}}.md',
|
||||||
|
[
|
||||||
|
'name' => $name,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
'type' => 'text/markdown',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)) (
|
||||||
|
\rosavox\services\doc\name($entry['id']),
|
||||||
|
\rosavox\services\doc\readable_path($entry['id'])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'area_download_audible' => (
|
||||||
|
(fn ($name, $path) => (
|
||||||
|
(! \file_exists($path))
|
||||||
|
?
|
||||||
|
\rosavox\helpers\misc\render(
|
||||||
|
'state-waiting',
|
||||||
|
[
|
||||||
|
'info' => \rosavox\helpers\misc\translate('state.generating'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
:
|
||||||
|
\rosavox\helpers\misc\render(
|
||||||
|
'download',
|
||||||
|
[
|
||||||
|
'text' => \rosavox\helpers\misc\translate('item.audible.short'),
|
||||||
|
'tooltip' => \rosavox\helpers\string_\coin(
|
||||||
|
'{{item}} {{action}}',
|
||||||
|
[
|
||||||
|
'action' => \rosavox\helpers\misc\translate('action.download'),
|
||||||
|
'item' => \rosavox\helpers\misc\translate('item.audible.long'),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
'link' => $path,
|
||||||
|
'name' => \rosavox\helpers\string_\coin(
|
||||||
|
'{{name}}.oga',
|
||||||
|
[
|
||||||
|
'name' => $name,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
'type' => 'audio/ogg',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)) (
|
||||||
|
\rosavox\services\doc\name($entry['id']),
|
||||||
|
\rosavox\services\doc\audible_path($entry['id'])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'area_hear' => (
|
||||||
|
(fn ($name, $path) => (
|
||||||
|
(! \file_exists($path))
|
||||||
|
?
|
||||||
|
\rosavox\helpers\misc\render(
|
||||||
|
'state-waiting',
|
||||||
|
[
|
||||||
|
'info' => \rosavox\helpers\misc\translate('state.generating'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
:
|
||||||
|
\rosavox\helpers\misc\render(
|
||||||
|
'player',
|
||||||
|
[
|
||||||
|
'source_path' => $path,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)) (
|
||||||
|
\rosavox\services\doc\name($entry['id']),
|
||||||
|
\rosavox\services\doc\audible_path($entry['id'])
|
||||||
|
)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
\rosavox\services\doc\list_()
|
\rosavox\services\doc\list_()
|
||||||
|
|
28
source/scripts/generate-audio
Executable file
28
source/scripts/generate-audio
Executable file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
## functions
|
||||||
|
|
||||||
|
function syntaxerror
|
||||||
|
{
|
||||||
|
echo "SYNTAX: generate-audio <input-path> <output-path>"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## args
|
||||||
|
|
||||||
|
if [ $# -ge 1 ] ; then input_path=$1 && shift ; else syntaxerror ; fi
|
||||||
|
if [ $# -ge 1 ] ; then output_path=$1 && shift ; else syntaxerror ; fi
|
||||||
|
|
||||||
|
|
||||||
|
## vars
|
||||||
|
|
||||||
|
key=$(cat "${input_path}" | sha256sum | cut -d ' ' -f 1)
|
||||||
|
wav_path=/tmp/${key}.wav
|
||||||
|
|
||||||
|
|
||||||
|
## exec
|
||||||
|
|
||||||
|
cat ${input_path} | piper/piper --model piper/voice.onnx --output_file ${wav_path} > /dev/null
|
||||||
|
ffmpeg -loglevel error -y -i ${wav_path} -vn ${output_path}
|
||||||
|
rm -f ${wav_path}
|
||||||
|
echo ${output_path}
|
|
@ -25,17 +25,17 @@ function name(int $id) : string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
function audio_path(int $id) : string
|
function readable_path(int $id) : string
|
||||||
{
|
{
|
||||||
return \sprintf('docs/%s.oga', name($id));
|
return \sprintf('docs/%s.md', name($id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
function readable_path(int $id) : string
|
function audible_path(int $id) : string
|
||||||
{
|
{
|
||||||
return \sprintf('docs/%s.md', name($id));
|
return \sprintf('docs/%s.oga', name($id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,28 @@ function generate_readable(int $id, $doc) : void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
function generate_audio(int $id, $doc) : void
|
function remove_readable(
|
||||||
|
int $id
|
||||||
|
) : void
|
||||||
|
{
|
||||||
|
$path = readable_path($id);
|
||||||
|
if (! \file_exists($path))
|
||||||
|
{
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
\unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
function generate_audible(
|
||||||
|
int $id,
|
||||||
|
$doc
|
||||||
|
) : void
|
||||||
{
|
{
|
||||||
$pause = " .\n";
|
$pause = " .\n";
|
||||||
$text = \rosavox\helpers\string_\coin(
|
$text = \rosavox\helpers\string_\coin(
|
||||||
|
@ -132,11 +153,32 @@ function generate_audio(int $id, $doc) : void
|
||||||
);
|
);
|
||||||
\rosavox\helpers\misc\generate_audio(
|
\rosavox\helpers\misc\generate_audio(
|
||||||
$text,
|
$text,
|
||||||
audio_path($id)
|
audible_path($id),
|
||||||
|
[
|
||||||
|
'blocking' => false,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
function remove_audible(
|
||||||
|
int $id
|
||||||
|
) : void
|
||||||
|
{
|
||||||
|
$path = audible_path($id);
|
||||||
|
if (! \file_exists($path))
|
||||||
|
{
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
\unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
function empty_() : array
|
function empty_() : array
|
||||||
|
@ -173,7 +215,7 @@ function create(array $doc) : int
|
||||||
{
|
{
|
||||||
$id = state::$storage->create($doc);
|
$id = state::$storage->create($doc);
|
||||||
generate_readable($id, $doc);
|
generate_readable($id, $doc);
|
||||||
generate_audio($id, $doc);
|
generate_audible($id, $doc);
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,8 +225,10 @@ function create(array $doc) : int
|
||||||
function update(int $id, array $doc) : void
|
function update(int $id, array $doc) : void
|
||||||
{
|
{
|
||||||
state::$storage->update($id, $doc);
|
state::$storage->update($id, $doc);
|
||||||
|
remove_readable($id);
|
||||||
|
remove_audible($id);
|
||||||
generate_readable($id, $doc);
|
generate_readable($id, $doc);
|
||||||
generate_audio($id, $doc);
|
generate_audible($id, $doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -193,6 +237,8 @@ function update(int $id, array $doc) : void
|
||||||
function delete(int $id) : void
|
function delete(int $id) : void
|
||||||
{
|
{
|
||||||
state::$storage->delete($id);
|
state::$storage->delete($id);
|
||||||
|
remove_readable($id);
|
||||||
|
remove_audible($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,7 +263,7 @@ function add_examples() : void
|
||||||
'authors' => [
|
'authors' => [
|
||||||
'Fenriswolf',
|
'Fenriswolf',
|
||||||
],
|
],
|
||||||
'content' => 'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen.',
|
'content' => 'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen. Diese Welt wird enden.',
|
||||||
'reasoning' => null,
|
'reasoning' => null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,12 @@
|
||||||
"action.delete": "löschen",
|
"action.delete": "löschen",
|
||||||
"action.hear": "hören",
|
"action.hear": "hören",
|
||||||
"action.read": "lesen",
|
"action.read": "lesen",
|
||||||
|
"action.download": "herunterladen",
|
||||||
|
"item.audible.short": "A",
|
||||||
|
"item.audible.long": "Audio",
|
||||||
|
"item.readable.short": "T",
|
||||||
|
"item.readable.long": "Text",
|
||||||
|
"state.generating": "wird erstellt",
|
||||||
"domain.doc.title": "Titel",
|
"domain.doc.title": "Titel",
|
||||||
"domain.doc.authors": "Autoren",
|
"domain.doc.authors": "Autoren",
|
||||||
"domain.doc.content": "Formulierung",
|
"domain.doc.content": "Formulierung",
|
||||||
|
|
|
@ -82,7 +82,22 @@ button
|
||||||
font-size: 1.0em;
|
font-size: 1.0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-list-entry
|
||||||
|
{
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-list-entry > *
|
.docs-list-entry > *
|
||||||
{
|
{
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.state-waiting
|
||||||
|
{
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download::before
|
||||||
|
{
|
||||||
|
content: "\21A7 ";
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<li class="docs-list-entry">
|
<li class="docs-list-entry">
|
||||||
<a href="{{value_link_open}}">{{value_text}}</a>
|
<a href="{{value_link_open}}">{{value_text}}</a>
|
||||||
|
|
|
|
||||||
<a href="{{value_link_read}}">[{{label_read}}]</a>
|
{{area_download_readable}}
|
||||||
|
|
|
|
||||||
<a href="{{value_link_hear}}" download="{{value_name}}" type="audio/ogg">[{{label_hear}}]</a>
|
{{area_download_audible}}
|
||||||
|
|
|
|
||||||
<audio controls="controls" src="{{value_link_hear}}"></audio>
|
{{area_hear}}
|
||||||
</li>
|
</li>
|
||||||
|
|
1
source/templates/download.html.tpl
Normal file
1
source/templates/download.html.tpl
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<a class="download" href="{{link}}" download="{{name}}" type="{{type}}" title="{{tooltip}}">{{text}}</a>
|
1
source/templates/player.html.tpl
Normal file
1
source/templates/player.html.tpl
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<audio controls="controls" src="{{source_path}}"></audio>
|
1
source/templates/state-waiting.html.tpl
Normal file
1
source/templates/state-waiting.html.tpl
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<span class="state-waiting" title="{{info}}">[~]</span>
|
Loading…
Add table
Reference in a new issue